package info.ebenoit.ebul.cmp;


import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import info.ebenoit.ebul.func.FunctionException;
import info.ebenoit.ebul.func.ThrowingBiConsumer;
import info.ebenoit.ebul.reflection.Classes;



public class ComponentRegistry
{
	private final class TempBuildRec
	{
		private final NewComponentInfo< ? > info;
		private final ComponentState state;
		private final HashMap< DependencyInfo , ComponentState > deps;
		private boolean added;


		private TempBuildRec( NewComponentInfo< ? > info )
		{
			this.info = info;
			this.state = new ComponentState( ComponentRegistry.this , info );
			this.deps = new HashMap< >( );
		}

	}

	private ArrayList< ComponentState > components;
	private HashMap< String , ComponentState > byName;
	private HashMap< Class< ? > , ArrayList< ComponentState > > byType;

	private boolean failed;
	private boolean initialised;
	private boolean active;


	public ComponentRegistry( )
	{
		this.components = new ArrayList< >( );
		this.byName = new HashMap< >( );
		this.byType = new HashMap< >( );
	}


	public boolean hasFailed( )
	{
		return failed;
	}


	public boolean isInitialised( )
	{
		return initialised;
	}


	public boolean isActive( )
	{
		return active;
	}


	public ComponentState getState( String name )
	{
		return this.byName.get( name );
	}


	public ComponentState getState( Class< ? > type )
			throws AmbiguousComponentException
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l == null ) {
			return null;
		}
		if ( l.size( ) == 1 ) {
			return l.get( 0 );
		}
		throw new AmbiguousComponentException( type , l.size( ) );
	}


	public List< ComponentState > getStates( Class< ? > type )
			throws AmbiguousComponentException
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l == null ) {
			return Collections.emptyList( );
		}
		return Collections.unmodifiableList( l );
	}


	public void getStates( Class< ? > type , Collection< ComponentState > states )
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l != null ) {
			int nFound = l.size( );
			for ( int i = 0 ; i < nFound ; i++ ) {
				states.add( l.get( i ) );
			}
		}
	}


	public List< ComponentState > getAllStates( )
	{
		return Collections.unmodifiableList( components );
	}


	public void getAllStates( Collection< ComponentState > result )
	{
		result.addAll( result );
	}


	public Object get( String name )
	{
		ComponentState cs = this.byName.get( name );
		return cs == null ? null : cs.getComponent( );
	}


	public < T > T get( Class< T > type )
			throws AmbiguousComponentException
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l == null ) {
			return null;
		}
		if ( l.size( ) == 1 ) {
			return type.cast( l.get( 0 ).getComponent( ) );
		}
		throw new AmbiguousComponentException( type , l.size( ) );
	}


	public < T > List< T > getAll( Class< T > type )
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l == null ) {
			return Collections.emptyList( );
		}

		int nFound = l.size( );
		ArrayList< T > result = new ArrayList< >( nFound );
		for ( int i = 0 ; i < nFound ; i++ ) {
			result.add( type.cast( l.get( i ).getComponent( ) ) );
		}
		return result;
	}


	public < T > void getAll( Class< T > type , Collection< T > result )
	{
		ArrayList< ComponentState > l = this.byType.get( type );
		if ( l != null ) {
			int nFound = l.size( );
			for ( int i = 0 ; i < nFound ; i++ ) {
				result.add( type.cast( l.get( i ).getComponent( ) ) );
			}
		}
	}


	public ComponentRegistry register( Collection< NewComponentInfo< ? > > components )
			throws ComponentCreationException , DuplicateComponentException , RecursiveDependenciesException ,
			DependencyInjectionException , AmbiguousComponentException , ComponentInitialisationException ,
			ComponentStartupException
	{
		if ( this.failed ) {
			throw new IllegalStateException( "register() called on failed registry" );
		}

		// Create "raw" state records for the new components
		int nAdd = components.size( );
		int nLeft = nAdd;
		ArrayList< TempBuildRec > adding = new ArrayList< >( nAdd );
		for ( NewComponentInfo< ? > nci : components ) {
			adding.add( new TempBuildRec( nci ) );
		}

		// Create the registry's new state
		ArrayList< ComponentState > nComponents = new ArrayList< >( this.components );
		HashMap< String , ComponentState > byName = new HashMap< >( this.byName );
		HashMap< Class< ? > , ArrayList< ComponentState > > byType = new HashMap< >( this.byType );

		// Add all new components whose dependencies are satisfied. Stop when there are no new components left. If there
		// are components left in the input list but no component can be added, it means there are some circular
		// dependencies.
		while ( nLeft > 0 ) {
			int found = resolveNewDependencies( adding , nComponents , byName , byType );
			if ( found == 0 ) {
				throw new RecursiveDependenciesException( adding.stream( ) //
						.filter( r -> !r.added ) //
						.map( r -> r.state.toString( ) ) //
						.collect( Collectors.toList( ) ) );
			}
			nLeft -= found;
		}

		// Set up driver components
		for ( int i = 0 ; i < nAdd ; i++ ) {
			TempBuildRec c = adding.get( i );
			String df = c.info.getDriverFor( );
			if ( df != null ) {
				c.state.setDriverFor( byName.get( df ) );
			}
		}

		// Inject and track dependencies
		injectDependencies( adding );

		// Replace registry contents
		int nOld = this.components.size( );
		this.components = nComponents;
		this.byName = byName;
		this.byType = byType;

		// Initialise all new components if the registry has been initialised
		if ( this.initialised ) {
			for ( int i = nOld ; i < nOld + nAdd ; i++ ) {
				initComponent( i );
			}
		}

		// Activate autostart components
		if ( this.active ) {
			for ( int i = 0 ; i < nOld + nAdd ; i++ ) {
				autostartComponent( i );
			}
		}

		return this;
	}


	public void initialise( )
			throws ComponentInitialisationException
	{
		if ( this.failed ) {
			throw new IllegalStateException( "initialise() called on failed registry" );
		}
		if ( this.initialised ) {
			return;
		}

		int nComponents = this.components.size( );
		for ( int i = 0 ; i < nComponents ; i++ ) {
			initComponent( i );
		}

		this.initialised = true;
	}


	public void destroy( )
			throws IllegalStateException , ComponentShutdownException , ComponentDestructionException
	{
		if ( this.failed ) {
			throw new IllegalStateException( "destroy() called on failed registry" );
		}
		if ( !this.initialised ) {
			return;
		}

		this.stop( );
		for ( int i = this.components.size( ) - 1 ; i >= 0 ; i-- ) {
			destroyComponent( i );
		}
		this.initialised = false;
	}


	public void start( )
			throws IllegalStateException , ComponentStartupException
	{
		if ( this.failed ) {
			throw new IllegalStateException( "start() called on failed registry" );
		}
		if ( !this.initialised ) {
			throw new IllegalStateException( "start() called on uninitialised registry" );
		}
		if ( this.active ) {
			return;
		}

		int nComponents = this.components.size( );
		for ( int i = 0 ; i < nComponents ; i++ ) {
			autostartComponent( i );
		}
		this.initialised = true;
	}


	public void stop( )
			throws IllegalStateException , ComponentShutdownException
	{
		if ( this.failed ) {
			throw new IllegalStateException( "stop() called on failed registry" );
		}
		if ( !this.active ) {
			return;
		}

		for ( int i = this.components.size( ) - 1 ; i >= 0 ; i-- ) {
			stopComponent( i );
		}
		this.active = false;
	}


	private static int resolveNewDependencies( ArrayList< TempBuildRec > adding ,
			ArrayList< ComponentState > nComponents , HashMap< String , ComponentState > byName ,
			HashMap< Class< ? > , ArrayList< ComponentState > > byType )
					throws AmbiguousComponentException , DuplicateComponentException
	{
		int size = adding.size( );
		int found = 0;

		for ( int i = 0 ; i < size ; i++ ) {
			TempBuildRec rec = adding.get( i );
			if ( rec.added || !checkDependencies( byName , byType , rec ) ) {
				continue;
			}

			// Add the component
			found++;
			nComponents.add( rec.state );
			String name = rec.state.getName( );
			if ( name != null ) {
				if ( byName.put( name , rec.state ) != null ) {
					throw new DuplicateComponentException( name );
				}
			}
			for ( Class< ? > cls : Classes.getAllTypes( rec.state.getComponent( ).getClass( ) ) ) {
				if ( cls == Object.class ) {
					continue;
				}
				ArrayList< ComponentState > l = byType.get( cls );
				if ( l == null ) {
					l = new ArrayList< >( );
					byType.put( cls , l );
				}
				l.add( rec.state );
			}
			rec.added = true;
		}

		return found;
	}


	private static boolean checkDependencies( HashMap< String , ComponentState > byName ,
			HashMap< Class< ? > , ArrayList< ComponentState > > byType , TempBuildRec rec )
					throws AmbiguousComponentException
	{
		boolean depsOk = true;
		for ( DependencyInfo dep : rec.info.getDependencies( ) ) {
			if ( rec.deps.containsKey( dep ) ) {
				continue;
			}

			ComponentState cs;
			if ( dep.getTargetClass( ) == null ) {
				cs = byName.get( dep.getTargetName( ) );
				continue;
			} else {
				ArrayList< ComponentState > lt = byType.get( dep.getTargetClass( ) );
				if ( lt == null ) {
					cs = null;
				} else if ( lt.size( ) == 1 ) {
					cs = lt.get( 0 );
				} else {
					throw new AmbiguousComponentException( dep.getTargetClass( ) , lt.size( ) );
				}
			}

			if ( cs == null ) {
				depsOk = false;
			} else {
				rec.deps.put( dep , cs );
			}
		}
		return depsOk;
	}


	private static void injectDependencies( ArrayList< TempBuildRec > adding )
			throws DependencyInjectionException
	{
		int size = adding.size( );

		for ( int i = 0 ; i < size ; i++ ) {
			TempBuildRec rec = adding.get( i );
			Object cmp = rec.state.getComponent( );
			for ( Map.Entry< DependencyInfo , ArrayList< ThrowingBiConsumer< Object , Object > > > di : rec.info
					.getInjectors( ).entrySet( ) ) {
				ComponentState toInject = rec.deps.get( di.getKey( ) );
				assert toInject != null;
				for ( ThrowingBiConsumer< Object , Object > action : di.getValue( ) ) {
					try {
						action.accept( cmp , toInject.getComponent( ) );
					} catch ( FunctionException e ) {
						throw new DependencyInjectionException( rec.state , toInject , e.getCause( ) );
					} catch ( RuntimeException e ) {
						throw new DependencyInjectionException( rec.state , toInject , e );
					}
				}
			}
		}

		for ( int i = 0 ; i < size ; i++ ) {
			TempBuildRec rec = adding.get( i );
			for ( ComponentState dep : rec.deps.values( ) ) {
				rec.state.addDependency( dep );
			}
		}
	}


	private void initComponent( int index )
			throws ComponentInitialisationException
	{
		try {
			this.components.get( index ).init( );
		} catch ( RuntimeException e ) {
			this.failed = true;
			throw e;
		}
	}


	private void autostartComponent( int index )
			throws ComponentStartupException
	{
		try {
			ComponentState cmp = this.components.get( index );
			if ( cmp.hasAutostart( ) ) {
				cmp.start( );
			}
		} catch ( RuntimeException e ) {
			this.failed = true;
			throw e;
		}
	}


	private void stopComponent( int index )
			throws ComponentDestructionException
	{
		try {
			this.components.get( index ).init( );
		} catch ( RuntimeException e ) {
			this.failed = true;
			throw e;
		}
	}


	private void destroyComponent( int index )
			throws ComponentDestructionException
	{
		try {
			this.components.get( index ).init( );
		} catch ( RuntimeException e ) {
			this.failed = true;
			throw e;
		}
	}
}