/*
 * Copyright (C) The Apache Software Foundation. All rights reserved.
 *
 * This software is published under the terms of the Apache Software License
 * version 1.1, a copy of which has been included with this distribution in
 * the LICENSE file.
 */
package org.apache.myrmidon.components.workspace;

import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.avalon.framework.activity.Disposable;
import org.apache.avalon.framework.activity.Initializable;
import org.apache.avalon.framework.component.ComponentException;
import org.apache.avalon.framework.component.ComponentManager;
import org.apache.avalon.framework.component.Composable;
import org.apache.avalon.framework.component.DefaultComponentManager;
import org.apache.avalon.framework.configuration.Configuration;
import org.apache.avalon.framework.logger.AbstractLoggable;
import org.apache.avalon.framework.parameters.ParameterException;
import org.apache.avalon.framework.parameters.Parameterizable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.log.Hierarchy;
import org.apache.log.Logger;
import org.apache.log.LogTarget;
import org.apache.myrmidon.api.DefaultTaskContext;
import org.apache.myrmidon.api.TaskContext;
import org.apache.myrmidon.api.TaskException;
import org.apache.myrmidon.components.deployer.DefaultDeployer;
import org.apache.myrmidon.components.deployer.Deployer;
import org.apache.myrmidon.components.deployer.DeploymentException;
import org.apache.myrmidon.components.executor.DefaultExecutionFrame;
import org.apache.myrmidon.components.executor.ExecutionFrame;
import org.apache.myrmidon.components.executor.Executor;
import org.apache.myrmidon.components.model.Project;
import org.apache.myrmidon.components.model.Target;
import org.apache.myrmidon.components.model.TypeLib;
import org.apache.myrmidon.components.type.TypeManager;
import org.apache.myrmidon.framework.Condition;
import org.apache.myrmidon.listeners.ProjectListener;

/**
 * This is the default implementation of Workspace.
 *
 * @author <a href="mailto:donaldp@apache.org">Peter Donald</a>
 */
public class DefaultWorkspace
    extends AbstractLoggable
    implements Workspace, Composable, Parameterizable, Initializable
{
    private Executor                 m_executor;
    private ProjectListenerSupport   m_listenerSupport   = new ProjectListenerSupport();
    private ComponentManager         m_componentManager;
    private Parameters               m_parameters;
    private TaskContext              m_baseContext;
    private HashMap                  m_entrys            = new HashMap();
    private TypeManager              m_typeManager;
    private Hierarchy                m_hierarchy;
    private int                      m_projectID;

    /**
     * Add a listener to project events.
     *
     * @param listener the listener
     */
    public void addProjectListener( final ProjectListener listener )
    {
        m_listenerSupport.addProjectListener( listener );
    }

    /**
     * Remove a listener from project events.
     *
     * @param listener the listener
     */
    public void removeProjectListener( final ProjectListener listener )
    {
        m_listenerSupport.removeProjectListener( listener );
    }

    /**
     * Retrieve relevent services needed for engine.
     *
     * @param componentManager the ComponentManager
     * @exception ComponentException if an error occurs
     */
    public void compose( final ComponentManager componentManager )
        throws ComponentException
    {
        m_componentManager = componentManager;
        m_typeManager = (TypeManager)componentManager.lookup( TypeManager.ROLE );
        m_executor = (Executor)componentManager.lookup( Executor.ROLE );
    }

    public void parameterize( final Parameters parameters )
        throws ParameterException
    {
        m_parameters = parameters;
    }

    public void initialize()
        throws Exception
    {
        m_baseContext = createBaseContext();

        m_hierarchy = new Hierarchy();

        final LogTarget target = new LogTargetToListenerAdapter( m_listenerSupport );
        m_hierarchy.setDefaultLogTarget( target );
    }

    /**
     * Execute a target in a particular project.
     * Execute in the project context.
     *
     * @param project the Project
     * @param target the name of the target
     * @exception TaskException if an error occurs
     */
    public void executeProject( final Project project, final String target )
        throws TaskException
    {
        final ProjectEntry entry = getProjectEntry( project );

        m_listenerSupport.projectStarted();

        executeTarget( "<init>", project.getImplicitTarget(), entry.getFrame() );

        execute( project, target, entry );

        m_listenerSupport.projectFinished();
    }


    private TaskContext createBaseContext()
        throws TaskException
    {
        final TaskContext context = new DefaultTaskContext();

        final String[] names = m_parameters.getNames();
        for( int i = 0; i < names.length; i++ )
        {
            final String value = m_parameters.getParameter( names[ i ], null );
            context.setProperty( names[ i ], value );
        }

        //Add system properties so that they overide user-defined properties
        addToContext( context, System.getProperties() );

        return context;
    }

    private File findTypeLib( final String libraryName )
        throws TaskException
    {
        //TODO: In future this will be expanded to allow
        //users to specify search path or automagically 
        //add entries to lib path (like user specific or 
        //workspace specific)
        final String name = libraryName.replace( '/', File.separatorChar ) + ".atl";

        final String home = System.getProperty( "myrmidon.home" );
        final File homeDir = new File( home + File.separatorChar + "ext" );
        
        final File library = new File( homeDir, name );

        if( library.exists() )
        {
            if( !library.canRead() )
            {
                throw new TaskException( "Unable to read library at " + library );
            }
            else
            {
                return library;
            }
        }

        throw new TaskException( "Unable to locate Type Library " + libraryName );
    }

    private void deployTypeLib( final Deployer deployer, final Project project )
        throws TaskException
    {
        final TypeLib[] typeLibs = project.getTypeLibs();

        for( int i = 0; i < typeLibs.length; i++ )
        {
            final TypeLib typeLib = typeLibs[ i ];
            final File file = findTypeLib( typeLib.getLibrary() );

            try
            {
                if( null == typeLib.getRole() )
                {
                    deployer.deploy( file );
                }
                else
                {
                    deployer.deployType( typeLib.getRole(), typeLib.getName(), file );
                }
            }
            catch( final DeploymentException de )
            {
                throw new TaskException( "Error deploying type library " +
                                         typeLib + " at " + file, de );
            }
        }
    }

    private ExecutionFrame createExecutionFrame( final Project project )
        throws TaskException
    {
        final TaskContext context = new DefaultTaskContext( m_baseContext );
        context.setProperty( TaskContext.BASE_DIRECTORY, project.getBaseDirectory() );

        //Create per frame ComponentManager
        final DefaultComponentManager componentManager =
            new DefaultComponentManager( m_componentManager );

        //Add in child type manager so each frame can register different
        //sets of tasks etc
        final TypeManager typeManager = m_typeManager.createChildTypeManager();
        componentManager.put( TypeManager.ROLE, typeManager );

        //We need to create a new deployer so that it deploys
        //to project specific TypeManager
        final DefaultDeployer deployer = new DefaultDeployer();
        deployer.setLogger( getLogger() );

        try { deployer.compose( componentManager ); }
        catch( final ComponentException ce )
        {
            throw new TaskException( "Error configuring deployer", ce );
        }

        //HACK: Didn't call initialize because Deployer contained in Embeddor
        // Already initialized and this would be reduendent
        //deployer.initialize();

        componentManager.put( Deployer.ROLE, deployer );

        deployTypeLib( deployer, project );

        //We need to place projects and ProjectManager
        //in ComponentManager so as to support project-local call()
        componentManager.put( Workspace.ROLE, this );
        componentManager.put( Project.ROLE, project );

        final String[] names = project.getProjectNames();
        for( int i = 0; i < names.length; i++ )
        {
            final String name = names[ i ];
            final Project other = project.getProject( name );
            componentManager.put( Project.ROLE + "/" + name, other );
        }

        final DefaultExecutionFrame frame = new DefaultExecutionFrame();

        try
        {
            final Logger logger = m_hierarchy.getLoggerFor( "project" + m_projectID );
            m_projectID++;

            frame.setLogger( logger );
            frame.contextualize( context );
            frame.compose( componentManager );
        }
        catch( final Exception e )
        {
            throw new TaskException( "Error setting up ExecutionFrame", e );
        }

        return frame;
    }

    private ProjectEntry getProjectEntry( final Project project )
        throws TaskException
    {
        ProjectEntry entry = (ProjectEntry)m_entrys.get( project );

        if( null == entry )
        {
            final ExecutionFrame frame = createExecutionFrame( project );
            entry = new ProjectEntry( project, frame );
            m_entrys.put( project, entry );
        }

        return entry;
    }

    private Project getProject( final String name, final Project project )
        throws TaskException
    {
        final Project other = project.getProject( name );

        if( null == other )
        {
            //TODO: Fix this so location information included in description
            throw new TaskException( "Project '" + name + "' not found." );
        }

        return other;
    }

    /**
     * Helper method to execute a target.
     *
     * @param project the Project
     * @param target the name of the target
     * @param context the context
     * @param done the list of targets already executed in current run
     * @exception TaskException if an error occurs
     */
    private void execute( final Project project,
                          final String targetName,
                          final ProjectEntry entry )
        throws TaskException
    {
        final int index = targetName.indexOf( "->" );
        if( -1 != index )
        {
            final String name = targetName.substring( 0, index );
            final String otherTargetName = targetName.substring( index + 2 );

            final Project otherProject = getProject( name, project );
            final ProjectEntry otherEntry = getProjectEntry( otherProject );

            //Execute target in referenced project
            execute( otherProject, otherTargetName, otherEntry );
            return;
        }

        final Target target = project.getTarget( targetName );
        if( null == target )
        {
            throw new TaskException( "Unable to find target " + targetName );
        }

        //add target to list of targets executed
        entry.completeTarget( targetName );

        //execute all dependencies
        final String[] dependencies = target.getDependencies();
        for( int i = 0; i < dependencies.length; i++ )
        {
            if( !entry.isTargetCompleted( dependencies[ i ] ) )
            {
                execute( project, dependencies[ i ], entry );
            }
        }

        //notify listeners
        m_listenerSupport.targetStarted( targetName );

        executeTarget( targetName, target, entry.getFrame() );

        //notify listeners
        m_listenerSupport.targetFinished();
    }

    /**
     * Method to execute a particular target instance.
     *
     * @param targetName the name of target
     * @param target the target
     * @param context the context in which to execute
     * @exception TaskException if an error occurs
     */
    private void executeTarget( final String name,
                                final Target target,
                                final ExecutionFrame frame )
        throws TaskException
    {
        //check the condition associated with target.
        //if it is not satisfied then skip target
        final Condition condition = target.getCondition();
        if( null != condition )
        {
            if( false == condition.evaluate( frame.getContext() ) )
            {
                getLogger().debug( "Skipping target " + name +
                                   " as it does not satisfy condition" );
                return;
            }
        }

        getLogger().debug( "Executing target " + name );

        //frame.getContext().setProperty( Project.TARGET, target );

        //execute all tasks assciated with target
        final Configuration[] tasks = target.getTasks();
        for( int i = 0; i < tasks.length; i++ )
        {
            executeTask( tasks[ i ], frame );
        }
    }

    /**
     * Execute a task.
     *
     * @param task the task definition
     * @param context the context
     * @exception TaskException if an error occurs
     */
    private void executeTask( final Configuration task, final ExecutionFrame frame )
        throws TaskException
    {
        final String name = task.getName();
        getLogger().debug( "Executing task " + name );

        //is setting name even necessary ???
        frame.getContext().setProperty( TaskContext.NAME, name );

        //notify listeners
        m_listenerSupport.taskStarted( name );

        //run task
        m_executor.execute( task, frame );

        //notify listeners task has ended
        m_listenerSupport.taskFinished();
    }

    /**
     * Helper method to add values to a context
     *
     * @param context the context
     * @param map the map of names->values
     */
    private void addToContext( final TaskContext context, final Map map )
        throws TaskException
    {
        final Iterator keys = map.keySet().iterator();

        while( keys.hasNext() )
        {
            final String key = (String)keys.next();
            final Object value = map.get( key );
            context.setProperty( key, value );
        }
    }
}
