

import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileList;

/**
 * Ant task to compose a sequence of source files or directories to a given
 * target file or directory.  The composition is done by parsing each
 * source file in order and composing the parse trees.
 *
 * @layer<antCompose>
 */
    
public class ComposeAntTask extends Task {
    public class SourceFile {

        /**
         * Returns the {@link File} representing this operand.
         *
         * @layer<antCompose>
         */
        public File getFile() {

            if ( file == null )
                throw new BuildException( "no name specified for file operand",
                                                getLocation() ) ;

            return getProject().resolveFile( file.getPath() ) ;
        }

        /**
         * Specifies the file for this operand.
         *
         * @layer<antCompose>
         */
        public void setName( File file ) {
            this.file = file ;
        }

        private File file = null ;

    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //

    /**
     * Specifies a single operand file to the composition.
     *
     * @layer<antCompose>
     */
    public void addFile( SourceFile file ) {
        operands.add( file ) ;
    }

    /**
     * Specifies a list of operand files to the composition.
     *
     * @layer<antCompose>
     */
    public void addFilelist( FileList files ) {
        operands.add( files ) ;
    }

    /**
     * Invoked when this task is next to be executed.
     *
     * @layer<antCompose>
     */
    public void execute() throws BuildException {

        if ( outputFile == null )
            throw new BuildException( "no output file given", getLocation() ) ;

        File[] inputFiles = getFiles() ;
        if ( inputFiles.length < 1 )
            throw new BuildException( "no input files given", getLocation() ) ;

        if ( upToDate( inputFiles, outputFile ) ) {
            log( "output file is up-to-date", Project.MSG_INFO ) ;
            return ;
        }

        try {
            compose( inputFiles, outputFile ) ;
        }
        catch ( BuildException exception ) {
            throw exception ;
        }
        catch ( Exception exception ) {
            throw new BuildException( exception, getLocation() ) ;
        }
    }

    /**
     * Returns an array of {@link File} objects listing all operands.
     *
     * @layer<antCompose>
     */
    public File[] getFiles() {

        if ( operands.size() < 1 )
            return new File [0] ;

        List files = new ArrayList() ;
        for ( Iterator p = operands.iterator() ; p.hasNext() ; ) {
            Object operand = p.next() ;
            if ( operand instanceof SourceFile )
                files.add( ( ( SourceFile ) operand ) . getFile() ) ;
            else
                if ( operand instanceof FileList )
                    files.addAll( asList( ( FileList ) operand ) ) ;
                else
                    throw new BuildException( "unexpected operand type",
                                                            getLocation() ) ;
        }

        return ( File[] ) files.toArray( new File [files.size()] ) ;
    }

    /**
     * Specifies the aspect name for the generated composition.
     *
     * @layer<antCompose>
     */
    public void setAspect( String aspectName ) {
        this.aspectName = aspectName ;
    }

    /**
     * Specifies whether the enclosing build should abort if the
     * composition fails.
     *
     * @layer<antCompose>
     */
    public void setFailonerror( boolean failOnError ) {
        this.failOnError = failOnError ;
    }

    /**
     * Specifies the output file (or directory) for the generated
     * composition.
     *
     * @layer<antCompose>
     */
    public void setOutput( File outputFile ) {
        this.outputFile = outputFile ;
    }

    /**
     * Specifies the output file (or directory) for the generated
     * composition; this is an alias for <code>output</code>.
     *
     * @layer<antCompose>
     */
    public void setTarget( File outputFile ) {
        this.outputFile = outputFile ;
    }

    // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - //

    /**
     * Returns the files listed in <code>fileList</code> as a {@link List}
     * of fully resolved {@link File} objects.
     *
     * @layer<antCompose>
     */
    final private List asList( FileList fileList ) {

        String[] names = fileList.getFiles( getProject() ) ;
        File[] files = new File [names.length] ;
        for ( int n = names.length ; --n >= 0 ; )
            files [n] = getProject().resolveFile( names [n] ) ;

        return Arrays.asList( files ) ;
    }

    /**
     * Composes a sequence of source files, writing the result into a
     * target file.
     *
     * @layer<antCompose>
     */
    final private void compose( File[] sources, File target )
    throws Exception {

        int errors =  AstNode.errorCount() ;

        log( "reading " + sources [0], Project.MSG_VERBOSE ) ;
        JTSParseTree composition =
                new  JTSParseTree( sources[0].getPath() ) ;

        for ( int n = 1 ; n < sources.length ; ++n ) {
            JTSParseTree extension =
                            new  JTSParseTree( sources[n].getPath() ) ;
            log( "reading " + sources [n], Project.MSG_VERBOSE ) ;
            composition.compose( extension ) ;
        }

        if ( aspectName != null )
            composition.setAspectName( aspectName ) ;

        if ( errors ==  AstNode.errorCount() ) {
            log( "writing " + target, Project.MSG_VERBOSE ) ;
            composition.print2file( target ) ;
        }

        if ( failOnError && errors >  AstNode.errorCount() )
            throw new BuildException( "composition failed with "
                            + ( AstNode.errorCount() - errors )
                            + " errors",
                            getLocation() ) ;
    }

    /**
     * Returns <code>true</code> if the given target file is more recent
     * than the specified source files.
     *
     * @layer<antCompose>
     */
    final private static boolean upToDate( File[] sources, File target ) {

        if ( ! target.exists() )
            return false ;

        long targetTime = target.lastModified() ;
        for ( int n = sources.length ; --n >= 0 ; )
            if ( sources[n].lastModified() >= targetTime )
                return false ;

        return true ;
    }

    private String aspectName = null ;
    private boolean failOnError = false ;
    private List operands = new ArrayList() ;
    private File outputFile = null ;

}
