/*
 * 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.frontends;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.apache.avalon.excalibur.cli.CLArgsParser;
import org.apache.avalon.excalibur.cli.CLOption;
import org.apache.avalon.excalibur.cli.CLOptionDescriptor;
import org.apache.avalon.excalibur.cli.CLUtil;
import org.apache.avalon.excalibur.io.ExtensionFileFilter;
import org.apache.avalon.framework.ExceptionUtil;
import org.apache.avalon.framework.logger.AbstractLoggable;
import org.apache.avalon.framework.parameters.Parameters;
import org.apache.log.Hierarchy;
import org.apache.log.LogTarget;
import org.apache.log.Logger;
import org.apache.log.Priority;
import org.apache.log.output.DefaultOutputLogTarget;
import org.apache.myrmidon.Constants;
import org.apache.myrmidon.api.DefaultTaskContext;
import org.apache.myrmidon.api.TaskContext;
import org.apache.myrmidon.api.TaskException;
import org.apache.myrmidon.components.builder.ProjectBuilder;
import org.apache.myrmidon.components.executor.Executor;
import org.apache.myrmidon.components.embeddor.Embeddor;
import org.apache.myrmidon.components.embeddor.DefaultEmbeddor;
import org.apache.myrmidon.components.workspace.Workspace;
import org.apache.myrmidon.components.model.Project;
import org.apache.myrmidon.listeners.ProjectListener;

/**
 * The class to kick the tires and light the fires.
 * Starts myrmidon, loads ProjectBuilder, builds project then uses ProjectManager
 * to run project.
 *
 * @author <a href="mailto:donaldp@apache.org">Peter Donald</a>
 */
public class CLIMain
    extends AbstractLoggable
{
    //defines for the Command Line options
    private static final int       HELP_OPT                  = 'h';
    private static final int       QUIET_OPT                 = 'q';
    private static final int       VERBOSE_OPT               = 'v';
    private static final int       FILE_OPT                  = 'f';
    private static final int       LOG_LEVEL_OPT             = 'l';
    private static final int       DEFINE_OPT                = 'D';
    private static final int       BUILDER_PARAM_OPT         = 'B';
    private static final int       VERSION_OPT               = 1;
    private static final int       LISTENER_OPT              = 2;
    private static final int       TASKLIB_DIR_OPT           = 5;
    private static final int       INCREMENTAL_OPT           = 6;
    private static final int       HOME_DIR_OPT              = 7;
    private static final int       DRY_RUN_OPT               = 8;

    //incompatable options for info options
    private static final int[]     INFO_OPT_INCOMPAT         = new int[]
    {
        HELP_OPT, QUIET_OPT, VERBOSE_OPT, FILE_OPT,
        LOG_LEVEL_OPT, VERSION_OPT, LISTENER_OPT,
        DEFINE_OPT, DRY_RUN_OPT //TASKLIB_DIR_OPT, HOME_DIR_OPT
    };

    //incompatable options for other logging options
    private static final int[]     LOG_OPT_INCOMPAT          = new int[]
    {
        QUIET_OPT, VERBOSE_OPT, LOG_LEVEL_OPT
    };

    private ProjectListener      m_listener;

    ///Parameters for run of myrmidon
    private Parameters           m_parameters  = new Parameters();

    ///List of targets supplied on command line to execute
    private ArrayList            m_targets     = new ArrayList();

    ///List of user supplied defines
    private Parameters           m_defines     = new Parameters();

    ///List of user supplied parameters for builder
    private Parameters           m_builderParameters = new Parameters();

    ///Determine whether tasks are actually executed
    private boolean              m_dryRun      = false;

    /**
     * Main entry point called to run standard Myrmidon.
     *
     * @param args the args
     */
    public static void main( final String[] args )
    {
        final CLIMain main = new CLIMain();

        try { main.execute( args ); }
        catch( final Throwable throwable )
        {
            System.err.println( "Error: " + ExceptionUtil.printStackTrace( throwable ) );
            System.exit( -1 );
        }

        System.exit( 0 );
    }

    /**
     * Display usage report.
     *
     */
    private void usage( final CLOptionDescriptor[] options )
    {
        System.out.println( "java " + getClass().getName() + " [options]" );
        System.out.println( "\tAvailable options:");
        System.out.println( CLUtil.describeOptions( options ) );
    }

    /**
     * Initialise the options for command line parser.
     */
    private CLOptionDescriptor[] createCLOptions()
    {
        //TODO: localise
        final CLOptionDescriptor[] options = new CLOptionDescriptor[ 13 ];

        options[0] =
            new CLOptionDescriptor( "help",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    HELP_OPT,
                                    "display this help message",
                                    INFO_OPT_INCOMPAT );

        options[1] =
            new CLOptionDescriptor( "file",
                                    CLOptionDescriptor.ARGUMENT_REQUIRED,
                                    FILE_OPT,
                                    "the build file." );

        options[2] =
            new CLOptionDescriptor( "log-level",
                                    CLOptionDescriptor.ARGUMENT_REQUIRED,
                                    LOG_LEVEL_OPT,
                                    "the verbosity level at which to log messages. " +
                                    "(DEBUG|INFO|WARN|ERROR|FATAL_ERROR)",
                                    LOG_OPT_INCOMPAT );

        options[3] =
            new CLOptionDescriptor( "quiet",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    QUIET_OPT,
                                    "equivelent to --log-level=FATAL_ERROR",
                                    LOG_OPT_INCOMPAT );

        options[4] =
            new CLOptionDescriptor( "verbose",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    VERBOSE_OPT,
                                    "equivelent to --log-level=INFO",
                                    LOG_OPT_INCOMPAT );

        options[5] =
            new CLOptionDescriptor( "listener",
                                    CLOptionDescriptor.ARGUMENT_REQUIRED,
                                    LISTENER_OPT,
                                    "the listener for log events." );

        options[6] =
            new CLOptionDescriptor( "version",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    VERSION_OPT,
                                    "display version",
                                    INFO_OPT_INCOMPAT );

        options[7] =
            new CLOptionDescriptor( "task-lib-dir",
                                    CLOptionDescriptor.ARGUMENT_REQUIRED,
                                    TASKLIB_DIR_OPT,
                                    "the task lib directory to scan for .tsk files." );
        options[8] =
            new CLOptionDescriptor( "incremental",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    INCREMENTAL_OPT,
                                    "Run in incremental mode" );
        options[9] =
            new CLOptionDescriptor( "myrmidon-home",
                                    CLOptionDescriptor.ARGUMENT_REQUIRED,
                                    HOME_DIR_OPT,
                                    "Specify myrmidon home directory" );
        options[10] =
            new CLOptionDescriptor( "define",
                                    CLOptionDescriptor.ARGUMENTS_REQUIRED_2,
                                    DEFINE_OPT,
                                    "Define a variable (ie -Dfoo=var)",
                                    new int[ 0 ] );

        options[11] =
            new CLOptionDescriptor( "builder-parameter",
                                    CLOptionDescriptor.ARGUMENTS_REQUIRED_2,
                                    BUILDER_PARAM_OPT,
                                    "Define a builder parameter (ie -Bfoo=var)" );

        options[12] =
            new CLOptionDescriptor( "dry-run",
                                    CLOptionDescriptor.ARGUMENT_DISALLOWED,
                                    DRY_RUN_OPT,
                                    "Do not execute tasks - just print them out" );

        return options;
    }

    private boolean parseCommandLineOptions( final String[] args )
    {
        final CLOptionDescriptor[] options = createCLOptions();
        final CLArgsParser parser = new CLArgsParser( args, options );

        if( null != parser.getErrorString() )
        {
            System.err.println( "Error: " + parser.getErrorString() );
            return false;
        }

        final List clOptions = parser.getArguments();
        final int size = clOptions.size();

        for( int i = 0; i < size; i++ )
        {
            final CLOption option = (CLOption)clOptions.get( i );

            switch( option.getId() )
            {
            case HELP_OPT: usage( options ); return false;
            case VERSION_OPT: System.out.println( Constants.BUILD_DESCRIPTION ); return false;

            case HOME_DIR_OPT: m_parameters.setParameter( "myrmidon.home", option.getArgument() ); break;
            case TASKLIB_DIR_OPT:
                m_parameters.setParameter( "myrmidon.lib.path", option.getArgument() );
                break;

            case LOG_LEVEL_OPT: m_parameters.setParameter( "log.level", option.getArgument() ); break;
            case VERBOSE_OPT: m_parameters.setParameter( "log.level", "INFO" ); break;
            case QUIET_OPT: m_parameters.setParameter( "log.level", "ERROR" ); break;

            case INCREMENTAL_OPT: m_parameters.setParameter( "incremental", "true" ); break;

            case FILE_OPT: m_parameters.setParameter( "filename", option.getArgument() ); break;
            case LISTENER_OPT: m_parameters.setParameter( "listener", option.getArgument() ); break;

            case DEFINE_OPT:
                m_defines.setParameter( option.getArgument( 0 ), option.getArgument( 1 ) );
                break;

            case BUILDER_PARAM_OPT:
                m_builderParameters.setParameter( option.getArgument( 0 ), option.getArgument( 1 ) );
                break;

            case DRY_RUN_OPT: m_dryRun = true; break;

            case 0: m_targets.add( option.getArgument() ); break;
            }
        }

        return true;
    }

    private void setupDefaultParameters()
    {
        //System property set up by launcher
        m_parameters.setParameter( "myrmidon.home", System.getProperty( "myrmidon.home", "." ) );

        m_parameters.setParameter( "filename", "build.ant" );
        m_parameters.setParameter( "log.level", "WARN" );
        m_parameters.setParameter( "listener", "org.apache.myrmidon.listeners.DefaultProjectListener" );
        m_parameters.setParameter( "incremental", "false" );
    }

    private void execute( final String[] args )
        throws Exception
    {
        setupDefaultParameters();

        if( !parseCommandLineOptions( args ) )
        {
            return;
        }

        //handle logging...
        final String logLevel = m_parameters.getParameter( "log.level", null );
        setLogger( createLogger( logLevel ) );

        final String home = m_parameters.getParameter( "myrmidon.home", null );
        final File homeDir = (new File( home )).getAbsoluteFile();
        if( !homeDir.isDirectory() )
        {
            throw new Exception( "myrmidon-home (" + homeDir + ") is not a directory" );
        }

        final String filename = m_parameters.getParameter( "filename", null );
        final File buildFile = (new File( filename )).getCanonicalFile();
        if( !buildFile.isFile() )
        {
            throw new Exception( "File " + buildFile + " is not a file or doesn't exist" );
        }

        //handle listener..
        final String listenerName = m_parameters.getParameter( "listener", null );
        final ProjectListener listener = createListener( listenerName );

        getLogger().warn( "Ant Build File: " + buildFile );
        getLogger().info( "Ant Home Directory: " + homeDir );
        //getLogger().info( "Ant Bin Directory: " + m_binDir );
        //getLogger().debug( "Ant Lib Directory: " + m_libDir );
        //getLogger().debug( "Ant Task Lib Directory: " + m_taskLibDir );

        if( m_dryRun )
        {
            m_parameters.setParameter( Executor.ROLE, 
                                       "org.apache.myrmidon.components.executor.PrintingExecutor" );
        }

        final Embeddor embeddor = new DefaultEmbeddor();
        setupLogger( embeddor );
        embeddor.parameterize( m_parameters );
        embeddor.initialize();
        embeddor.start();

        //create the project
        final Project project = 
            embeddor.createProject( buildFile.toString(), null, m_builderParameters );

        BufferedReader reader = null;

        //loop over build if we are in incremental mode..
        final boolean incremental = m_parameters.getParameterAsBoolean( "incremental", false );
        while( true )
        {
            //actually do the build ...
            final Workspace workspace = embeddor.createWorkspace( m_defines );
            workspace.addProjectListener( listener );

            doBuild( workspace, project, m_targets );

            if( !incremental ) break;

            System.out.println( "Continue ? (Enter no to stop)" );

            if( null == reader )
            {
                reader = new BufferedReader( new InputStreamReader( System.in ) );
            }

            String line = reader.readLine();

            if( line.equalsIgnoreCase( "no" ) ) break;

        }

        embeddor.stop();
        embeddor.dispose();
    }

    /**
     * Actually do the build.
     *
     * @param manager the manager
     * @param project the project
     * @param targets the targets to build as passed by CLI
     */
    private void doBuild( final Workspace workspace,
                          final Project project,
                          final ArrayList targets )
    {
        try
        {
            final int targetCount = targets.size();

            //if we didn't specify a target on CLI then choose default
            if( 0 == targetCount )
            {
                workspace.executeProject( project, project.getDefaultTargetName() );
            }
            else
            {
                for( int i = 0; i < targetCount; i++ )
                {
                    workspace.executeProject( project, (String)targets.get( i ) );
                }
            }
        }
        catch( final TaskException ae )
        {
            getLogger().error( "BUILD FAILED" );
            getLogger().error( "Reason:\n" + ExceptionUtil.printStackTrace( ae, 5, true ) );
        }
    }

    /**
     * Create Logger of appropriate log-level.
     *
     * @param logLevel the log-level
     * @return the logger
     * @exception Exception if an error occurs
     */
    private Logger createLogger( final String logLevel )
        throws Exception
    {
        final String logLevelCapitalized = logLevel.toUpperCase();
        final Priority priority = Priority.getPriorityForName( logLevelCapitalized );

        if( !priority.getName().equals( logLevelCapitalized ) )
        {
            throw new Exception( "Unknown log level - " + logLevel );
        }

        final Logger logger = Hierarchy.getDefaultHierarchy().getLoggerFor( "myrmidon" );

        final DefaultOutputLogTarget target = new DefaultOutputLogTarget();
        target.setFormat( "[%8.8{category}] %{message}\\n%{throwable}" );
        logger.setLogTargets( new LogTarget[] { target } );

        logger.setPriority( priority );

        return logger;
    }

    /**
     * Setup project listener.
     *
     * @param listener the classname of project listener
     */
    private ProjectListener createListener( final String listener )
        throws Exception
    {
        try { return (ProjectListener)Class.forName( listener ).newInstance(); }
        catch( final Throwable t )
        {
            throw new Exception( "Error creating the listener " + listener +
                                 " due to " + ExceptionUtil.printStackTrace( t, 5, true ) );
        }
    }

    /**
     * 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 Exception
    {
        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 );
        }
    }
}

