/* *******************************************************************
 * Copyright (c) 2000-2001 Xerox Corporation. 
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Common Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/cpl-v10.html 
 *  
 * Contributors: 
 *     Xerox/PARC     initial implementation 
 * ******************************************************************/

package org.aspectj.tools.ant.taskdefs;

import org.apache.tools.ant.*;
import org.apache.tools.ant.types.*;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.taskdefs.*;
import java.io.*;
import java.util.List;
import java.util.Iterator;
import java.util.ArrayList;
import java.util.StringTokenizer;

/**
 * Ant task for the AspectJ compiler -- AJC.
 * List (.lst) files are passed in as includes.
 *
 * This task was developed by the <a href="http://aspectj.org">AspectJ Project</a>
 *
 * @author <a href="mailto:palm@parc.xerox.com">Jeffrey Palm</a>
 * @see    org.aspectj.tools.ant.taskdefs.compilers.AjcCompiler
 */
public class Ajc2 extends Javac {

    /**
     * The name of the adapter we use.
     */
    public final static String ADAPTER_CLASS =
        "org.aspectj.tools.ant.taskdefs.compilers.Ajc";

    /* ----------------------------------------------------------------------
     * Attribute members
     * ----------------------------------------------------------------------
     */
   
    /**
     * How many concurrent threads to use for compilation,
     * defaults to 0 -- multi-threading disabled.
     */
    private Integer threads;

    /**
     * Don't generate any comments into the woven code.
     */
    private boolean nocomments;

    /**
     * Don't generate .ajsym or .ajsline files.
     */
    private boolean nosymbols;

    /**
     * Generate regular Java code into <workingdir>.
     */
    private boolean preprocess;

    /**
     * Specify where to place intermediate .java files <dir>
     * defaults to ./ajworkingdir.
     */
    private File workingdir;

    /**
     * The file is a line-delimited list of arguments
     * these arguments are inserted into the argument list
     */
    private List argfiles;


    /* ----------------------------------------------------------------------
     * Misc. members
     * ----------------------------------------------------------------------
     */

    /**
     * Whether we have used the <code>excludes</code> attribute.
     */
    private boolean haveExcludes = false;

    /**
     * Whether we have used the <code>includes</code> attribute.
     */
    private boolean haveIncludes = false;

    /**
     * Whether we have used the <code>excludes</code> attribute.
     * @return Whether we have used the <code>excludes</code> attribute.
     */
    protected boolean hasExcludes() {
        return haveExcludes;
    }

    /**
     * Whether we have used the <code>includes</code> attribute.
     * @return Whether we have used the <code>includes</code> attribute.
     */
    protected boolean hasIncludes() {
        return haveIncludes;
    }

    /* ----------------------------------------------------------------------
     * Attribute access methods
     * ----------------------------------------------------------------------
     */
    
    /**
     * Sets the number of threads.
     *
     * @param threads the number of threads.
     * @see           Ajc2#threads
     */
    public void setThreads(Integer threads) {
        this.threads = threads;
    }

    /**
     * Returns the number of threads.
     *
     * @return the number of threads.
     * @see    Ajc2#threads
     */
    public Integer getThreads() {
        return threads;
    }

    /**
     * Set the <code>-nocomments</code> flag.
     *
     * @param nocomments true turns on the flag.
     * @see              Ajc2#nocomments
     */
    public void setNocomments(boolean nocomments) {
        this.nocomments = nocomments;
    }

    /**
     * Returns if the <code>-nocomments</code> flag is turned on.
     *
     * @return <code>true</code> if the <code>-nocomments</code> flag is on.
     * @see    Ajc2#nocomments
     */
    public boolean getNocomments() {
        return nocomments;
    }

    /**
     * Set the <code>-nosymbols</code> flag.
     *
     * @param nosymbols true turns on the flag.
     * @see             Ajc2#nosymbols
     */
    public void setNosymbols(boolean nosymbols) {
        this.nosymbols = nosymbols;
    }

    /**
     * Returns if the <code>-nosymbols</code> flag is turned on.
     *
     * @return <code>true</code> if the <code>-nosymbols</code> flag is on.
     * @see    Ajc2#nosymbols
     */    
    public boolean getNosymbols() {
        return nosymbols;
    }

    /**
     * Returns if the <code>-preprocess</code> flag is turned on.
     *
     * @return <code>true</code> if the <code>-preprocess</code> flag is on.
     * @see    Ajc2#preprocess
     */
    public void setPreprocess(boolean preprocess) {
        this.preprocess = preprocess;
    }

    /**
     * Returns if the <code>-preprocess</code> flag is turned on.
     *
     * @return <code>true</code> if the <code>-preprocess</code> flag is on.
     * @see    Ajc2#preprocess
     */      
    public boolean getPreprocess() {
        return preprocess;
    }

    /**
     * Sets the workingdir.
     *
     * @param workingdir the new workingdir.
     * @see   Ajc2#workingdir
     */
    public void setWorkingdir(File workingdir) {
        this.workingdir = workingdir;
    }

    /**
     * Returns the current workingdir.
     *
     * @return the current workingdir.
     * @see    Ajc2#workingdir
     */
    public File getWorkingdir() {
        return workingdir;
    }

    /**
     * Sets the the argument files by the comma-delimited String passed in.
     *
     * @param argfiles comma-delimited String contained argument files.
     */
    public void setArgfiles(String argfiles) {
        StringTokenizer tok = new StringTokenizer(argfiles, ",");
        while (tok.hasMoreTokens()) {
            File argfile = project.resolveFile(tok.nextToken());
            if (argfile != null && argfile.exists() && !argfile.isDirectory()) {
                createArgfile().setFile(argfile);
            }
        }
    }

    /**
     * Creates a nested <code>Argfile</code>, add it to the list
     * <code>argfiles</code>, and returns the new <code>Argfile</code>
     * instance.
     *
     * @return a new <code>Argfile</code> instance.
     */
    public Argfile createArgfile() {
        Argfile argfile = new Argfile();
        if (argfiles == null) {
            argfiles = new ArrayList();
        }
        argfiles.add(argfile);
        return argfile;
    }

    /**
     * Returns the java.util.List of argfiles.
     * This could be <code>null</code>.
     *
     * @return the list of argfiles.
     */
    public List getArgfiles() {
        return argfiles;
    }

    /**
     * A simple class with one member -- <code>file</code> -- that
     * represents an argument file.
     */
    public static class Argfile {
        private File file;
        public void setFile(File file) { this.file = file; }
        public File getFile() { return file; }
        public String toString() { return file.getAbsolutePath(); }
    }

    /* ----------------------------------------------------------------------
     * Misc. methods
     * ----------------------------------------------------------------------
     */

    /* Leaving the includes blank in the Javac task defaults to including all.
     * In Ajc, if the user specifies an argfile, but leaves the includes and
     * excludes blank that person probably meant to just includes to argfile.
     * So, we keep track of whether includes and/or excludes have been explicitly
     * used with the have{In,Ex}cludes members, and setting these flags
     * in the create... and set... methods.
     *
     * When constructing the compileList, if both haveIncludes and haveExcludes
     * are false, but the user has specified an argfile, then we set includes
     * to '!**' before adding the contents of the argfile.
     */

    /**
     * Override Javac.createInclude() to set <code>haveIncludes</code>
     * to <code>true</code>.
     *
     * @return new PatternSet.NameEntry to be added to the include list.
     * @see    org.apache.tools.taskdefs.Javac#createInclude()
     */
    public PatternSet.NameEntry createInclude() {
        haveIncludes = true;
        return super.createInclude();
    }

    /**
     * Override Javac.createExclude() to set <code>haveExcludes</code>
     * to <code>true</code>.
     *
     * @return new PatternSet.NameEntry to be added to the exclude list.
     * @see    org.apache.tools.taskdefs.Javac#createExclude()
     */    
    public PatternSet.NameEntry createExclude() {
        haveExcludes = true;
        return super.createExclude();
    }

    /**
     * Override Javac.setIncludes(String) to set <code>haveIncludes</code>
     * to <code>true</code>.
     *
     * @param includes Comma-separated list of includes.
     * @see   org.apache.tools.taskdefs.Javac#setIncludes(java.lang.String)
     */
    public void setIncludes(String includes) {
        haveIncludes = true;
        super.setIncludes(includes);
    }

    /**
     * Override Javac.setExcludes(String) to set <code>haveExcludes</code>
     * to <code>true</code>.
     *
     * @param excludes Comma-separated list of excludes.
     * @see   org.apache.tools.taskdefs.Javac#setExcludes(java.lang.String)
     */    
    public void setExcludes(String excludes) {
        haveExcludes = true;
        super.setExcludes(excludes);
    }

    public String getAdapterClass() {
        return ADAPTER_CLASS;
    }
    

    public final void execute() throws BuildException {
        prepare();
        executeAfterPrepare();
    }

    /**
     * Executes by first setting the <code>build.compiler</code> property
     * to AjcCompiler, then invokes the super.execute() method.
     *
     * @throws org.apache.tools.ant.BuildException
     * @see    org.apache.tools.ant.taskdefs.Javac#execute()
     */    
    public void executeAfterPrepare() throws BuildException {
        
        // Save the old build.compiler property
        String oldBuildCompiler = project.getProperty("build.compiler");

        // If oldBuildCompiler is null try to resolve it
        if (oldBuildCompiler == null) {
            String javaVersion = Project.getJavaVersion();
            if (javaVersion.equals(Project.JAVA_1_0)) {
                // Cannot happen
            } else if (javaVersion.equals(Project.JAVA_1_1)) {
                oldBuildCompiler = "classic";
            } else if (javaVersion.equals(Project.JAVA_1_2)) {
                oldBuildCompiler = "classic";
            } else if (javaVersion.equals(Project.JAVA_1_3)) {
                oldBuildCompiler = "modern";
            }
        }

        // Set the new adapter
        project.setProperty("build.compiler", getAdapterClass());
        BuildException caught = null;
        try {
            super.execute();
        } catch (BuildException be) {
            caught = be;
        } finally {
            
            // Reset to the old compiler
            if (oldBuildCompiler != null) {
                project.setProperty("build.compiler", oldBuildCompiler);
            }
        }

        // If we caught an exception executing throw it
        if (caught != null) {
            throw caught;
        }
     }

    /**
     * Guaranteed to be called before doing real execute.
     */
    public void prepare() {
        if (argfiles != null && !haveIncludes &&
            !haveExcludes && getSrcdir() == null) {
            useDefaultSrcdir();
        }
    }

    protected final void useDefaultSrcdir() {
        setSrcdir(new Path(project, "."));
    }

    /**
     * Overrides Javac.scanDir(..) so that it doesn't check dependencies.
     *
     * @see org.apache.tools.ant.taskdefs.Javac#scanDir
     */
    protected void scanDir(File srcDir, File destDir, String files[]) {
        List newFiles = new ArrayList();

        // Add the files listed in the argfiles to the includes
        List newIncludes = new ArrayList();
        List newArguments = new ArrayList();
        if (argfiles != null) {
            Iterator iter = argfiles.iterator();
            while (iter.hasNext()) {
                File argfile = ((Argfile)iter.next()).getFile();
                expandArgfile(argfile, newIncludes, newArguments);
            }
        }

        // If there aren't any includes, but we've used an argfile then we should
        // set the includes to be the one's found in newIncludes
        // If we do this, we need to re-read files from the directory scanner
        if (!haveIncludes && !haveExcludes && argfiles != null) {
            log("Setting includes to '!**'", Project.MSG_VERBOSE);
            setIncludes("!**");
            //files = getDirectoryScanner(srcDir).getIncludedFiles();
        }

        // Otherwise we want to add all .java files to the compileList
        else {
            for (int i = 0; i < files.length; i++) {
                File newFile = new File(srcDir, files[i]);
                if (newFile != null &&
                    newFile.exists() &&
                    newFile.getName().endsWith(".java")) {
                    newFiles.add(newFile);
                }
            }
        }

        // Add the new included files
        Iterator iter = newIncludes.iterator();
        while (iter.hasNext()) {
            newFiles.add((File)iter.next());
        }

        // This is the same behavior found in Javac
        int newFileSize = newFiles.size();
        if (newFileSize > 0) {
            File[] newCompileList = new File[compileList.length + newFileSize];
            System.arraycopy(compileList, 0, newCompileList, 0, compileList.length);
            System.arraycopy(newFiles.toArray(), 0, newCompileList,
                             compileList.length, newFileSize);
            compileList = newCompileList;
        }
    }

    private void expandArgfile(File argfile, List includes, List arguments) {

        log("argfile:" + argfile, Project.MSG_VERBOSE);
        
        // All paths are relative to the parent
        File parent = argfile.getParentFile();

        // Sanity check
        if (parent == null || !parent.exists() || !parent.isDirectory()) {
            return;
        }

        // Read the file
        BufferedReader in = null;
        try {
            in = new BufferedReader(new FileReader(argfile));
            String line;
            while ((line = in.readLine()) != null) {
                line = line.trim();

                // Skip blank lines
                if ("".equals(line)) {
                    continue;
                }

                // Allow '#' and '//' line comments
                int isharp = line.indexOf("#");
                if (isharp != -1) {
                    line = line.substring(0, isharp);
                }

//                int istar = -1;

                // Argument
                if (line.startsWith("-")) {
                    arguments.add(line);
                }

                // If there are stars we'll try to resolve the file here
                else if (line.indexOf("*") != -1) {
                    log("The argfile line '" + line + "' is invalid",
                        Project.MSG_WARN);
                }

                // Another argfile
                else if (line.startsWith("@")) {
                    String newArgfileName = line.substring(1);
                    File newArgfile = new File(parent, newArgfileName);
                    expandArgfile(newArgfile, includes, arguments);
                }

                // Source file
                else {
                    File newfile = new File(line);
                    if (!newfile.isAbsolute()) {
                        newfile = new File(parent, line);
                    }
                    if (newfile != null && newfile.exists() &&
                        !newfile.isDirectory() &&
                        newfile.getName().endsWith(".java")) {
                        includes.add(newfile);
                    }
                }

            }
        } catch (IOException ioe) {
            log("trouble with argfile: " + argfile + ":" + ioe, Project.MSG_ERR);
        } finally {
            try {
                if (in != null) {
                    in.close();
                }
            } catch (IOException ioe2) {
            }
        }
    }
}
