import java.io.File;
import java.util.*;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.types.Commandline;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.util.GlobPatternMapper;
import org.apache.tools.ant.util.SourceFileScanner;

import polyglot.main.Main.TerminationException;

/**
 * An ANT {@code Task} to facilitate the building of
 * of code written in language extensions within the ANT framework. This task
 * can be used similarly way to the {@code JavaC} task. 
 * 
 * <p> This class can
 * optionally be subclassed by extensions (to provide compilation for 
 * a particular language). In that case, subclasses should override the
 * init method to set the source file extension 
 * ({@link PolyglotAntTask#srcExt srcExt}) and extension name
 * or class ({@link PolyglotAntTask#extensionName extensionName} or 
 * {@link PolyglotAntTask#extensionClass extensionClass}) appropriately, and 
 * override the setter methods for these fields to prevent the user from modifying
 * these fields.
 * 
 * <b>This class has not been fully tested.</b>
 * 
 * @see org.apache.tools.ant.Task
 */
public class PolyglotAntTask extends MatchingTask {
    /**
     * Name of the extension, like the {@code -ext} command line option.
     */
    protected String extensionName;
    /**
     * Class of the extension, like the {@code -extclass} command line 
     * option.
     */
    protected Class extensionClass;
    /**
     * Destination directory, like the {@code -d} command line option.
     */
    protected File destDir;
    /**
     * The path for source compilation.
     */
    protected Path src;
    
    /**
     * Bootclasspath, like the {@code -bootclasspath} command line option.
     */
    protected Path bootclasspath;
    /**
     * The file extension for sourcefiles, like the {@code -sx}
     * command line option.
     */
    protected String srcExt;
    /**
     * The file extensions for output files, like the {@code -ox}
     * command line option.
     */
    protected String outputExt;
    /**
     * A file containing additional command line options to parse, like
     * the {@code <file>} command line option.
     */
    protected File optionsFile;
    
    /**
     * The post compiler to invoke, like the {@code -post}
     * command line option.
     */
    protected String post;
    /**
     * Indiciates if the Polyglot compiler should produce serialized type
     * information in the output files, like the {@code -noserial}
     * command line option.
     */
    protected boolean noSerial;
    /**
     * Indiciates if the Polyglot compiler should delete the output files
     * after compilation, like the {@code -nooutput} command line option.
     */
    protected boolean noOutput;
    /**
     * Indiciates if the Polyglot compiler should use fully qualified
     * class names, like the {@code -fqcn}
     * command line option.
     */
    protected boolean fqcn;
    /**
     * Indiciates if the Polyglot compiler should use only compile to the 
     * output files, like the {@code -c}
     * command line option.
     */
    protected boolean onlyToJava;
    
    /**
     * A {@code List} of {@link org.apache.tools.ant.types.Commandline.Arguments 
     * Commandline.Arguments}, to allow the user to specify command line 
     * options that we do not deal with explicitly.
     */
    protected List additionalArgs; 
    
    /**
     * The classpath to be used for compilation, like the {@code -classpath}
     * command line option.
     */
    protected Path compileClasspath;
    /**
     * The path for finding source files, like the {@code -sourcepath}
     * command line option.
     */
    protected Path compileSourcepath;

    public PolyglotAntTask() { }
    
    public void init() { 
        super.init();
        extensionName = null;
        extensionClass = null;
        destDir = null;
        src = null;
        bootclasspath = null;
        srcExt = "java";
        outputExt = "java";
        optionsFile = null;
        post = null;
        noSerial = false;
        noOutput = false;
        fqcn = false;
        onlyToJava = false;
        additionalArgs = null;
        compileClasspath = null;
        compileSourcepath = null;
    }
    
    public void execute() throws BuildException {
        dumpDetails();
        checkParameters();
        // scan source directories and dest directory to build up
        File[] compileFiles = getCompileFiles();
        
        if (compileFiles.length == 0) {
            // nothing to do. no files to compile
            log("No files to compile", 
                Project.MSG_INFO);
            return;
        }
        
        // build up the argument list
        String[] args = constructArgList(compileFiles);
        
        log("Invoking polyglot with the following arguments: " + 
            "(enclosing single quotes are not part of the arguments):", 
            Project.MSG_VERBOSE);
        for (int i = 0; i < args.length; i++) {
            log("  '" + args[i] + "'", Project.MSG_VERBOSE);                
        }
        // invoke the polyglot compiler.
        compile(args);
    }
    
    protected File[] getCompileFiles() {
        File[] compileFiles = new File[0];
        String[] srcList = src.list();
        for (int i = 0; i < srcList.length; i++) {
            File srcDir = getProject().resolveFile(srcList[i]);
            if (!srcDir.exists()) {
                throw new BuildException("srcdir \""
                                         + srcDir.getPath()
                                         + "\" does not exist!", getLocation());
            }

            DirectoryScanner ds = this.getDirectoryScanner(srcDir);
            String[] files = ds.getIncludedFiles();

            compileFiles = scanDir(compileFiles, srcDir, destDir != null ? destDir : srcDir, files);
        }
        return compileFiles;
    }
    
    /**
     * Scans the directory looking for source files to be compiled.
     * The results are returned in the class variable compileList
     */
    protected File[] scanDir(File[] compileFiles, File srcDir, File destDir, String[] files) {
        GlobPatternMapper m = new GlobPatternMapper();
        m.setFrom("*." + srcExt);
        m.setTo("*." + destFileExt());
        SourceFileScanner sfs = new SourceFileScanner(this);
        File[] newFiles = sfs.restrictAsFiles(files, srcDir, destDir, m);

        if (newFiles.length > 0) {
            File[] newCompileList = new File[compileFiles.length + 
                newFiles.length];
            System.arraycopy(compileFiles, 0, newCompileList, 0,
                compileFiles.length);
            System.arraycopy(newFiles, 0, newCompileList,
                compileFiles.length, newFiles.length);
            compileFiles = newCompileList;
        }
        return compileFiles;
    }

    /**
     * The file extension of the destination files. Used by the global pattern
     * matcher to determine which files need to be compiled.
     */
    protected String destFileExt() {
        return onlyToJava ? outputExt : "class";
    }
    
    /**
     * Check to make sure the paramters are consistent with invoking the 
     * Polyglot compiler. 
     * @throws BuildException if the parameters are inconsistent.
     */
    protected void checkParameters() throws BuildException {
        // at most one of extenstionClass and extensionName is set.
        if (extensionClass != null && extensionName != null) {
            throw new BuildException("At most one of extclass and " + 
                                     "extname can be set.", getLocation());
        }
        
        if (src == null) {
            throw new BuildException("srcdir attribute must be set!",
                                     getLocation());
        }
        if (src.size() == 0) {
            throw new BuildException("srcdir attribute must be set!",
                                     getLocation());
        }

        if (destDir != null && !destDir.isDirectory()) {
            throw new BuildException("destination directory \"" 
                                     + destDir 
                                     + "\" does not exist "
                                     + "or is not a directory", getLocation());
        }
    }

    /**
     * Construct the argument list that will be given to the Polyglot
     * compiler.
     * @param files the files to compile
     */
    protected String[] constructArgList(File[] files) {
        List argList = new ArrayList();
        
        // set up command line args
        if (optionsFile != null) {
            argList.add("@" + optionsFile.getPath());
        }
        if (extensionName != null) {
            argList.add("-ext");
            argList.add(extensionName);
        }
        if (extensionClass != null) {
            argList.add("-extclass");
            argList.add(extensionClass.getName());
        }
        if (noSerial) {
            argList.add("-noserial");
        }
        if (noOutput) {
            argList.add("-nooutput");
        }
        if (fqcn) {
            argList.add("-fqcn");
        }
        if (onlyToJava) {
            argList.add("-c");
        }
        if (post != null) {
            argList.add("-post");
            argList.add(post);
        }
        if (srcExt != null) {
            argList.add("-sx");
            argList.add(srcExt);
        }
        if (outputExt != null) {
            argList.add("-ox");
            argList.add(outputExt);
        }
        if (destDir != null) {
            argList.add("-d");
            argList.add(destDir.getPath());
        }
        if (compileSourcepath != null) {
            argList.add("-sourcepath");
            argList.add(compileSourcepath.toString());
        }
        else if (src != null) {
            argList.add("-sourcepath");
            argList.add(src.toString());
        }
        if (bootclasspath != null) {
            argList.add("-bootclasspath");
            argList.add(bootclasspath.toString());
        }
        if (compileClasspath != null) {
            argList.add("-classpath");
            argList.add(compileClasspath.toString());
        }
        if (additionalArgs != null) {
            for (Iterator i = additionalArgs.iterator(); i.hasNext(); ) {
                Commandline.Argument arg = (Commandline.Argument)i.next();
                String[] parts = arg.getParts();
                for (int j = 0; j < parts.length; j++) {
                    argList.add(parts[j]);
                }
            }
        }
        
        // add the files to compile.
        int argListSize = argList.size();
        String[] args = new String[argListSize + files.length];
        argList.toArray(args);
        
        for (int i = 0; i < files.length; ++i) {
            args[argListSize + i] = files[i].getPath();
        }
        
        return args;
    }
    
    /**
     * Invoke the Polyglot compiler, with the given command line arguments.
     */
    protected void compile(String[] args) {
        try {
            new polyglot.main.Main().start(args);
        }
        catch (TerminationException e) {
            throw new BuildException(e.getMessage(), e);
        }
    }
     
    /**
     * Dump the settings of the parameters, for debugging purposes.
     */  
    protected void dumpDetails() {
        log("extensionName = " + extensionName, Project.MSG_DEBUG);
        log("extensionClass = " + extensionClass, Project.MSG_DEBUG);
        log("destdir = " + destDir, Project.MSG_DEBUG);
        log("src = " + src, Project.MSG_DEBUG);
        log("bootclasspath = " + bootclasspath, Project.MSG_DEBUG);
        log("srcExt = " + srcExt, Project.MSG_DEBUG);
        log("outputExt = " + outputExt, Project.MSG_DEBUG);
        log("optionsFile = " + optionsFile, Project.MSG_DEBUG);
        log("post = " + post, Project.MSG_DEBUG);
        log("noSerial = " + noSerial, Project.MSG_DEBUG);
        log("noOutput = " + noOutput, Project.MSG_DEBUG);
        log("fqcn = " + fqcn, Project.MSG_DEBUG);
        log("onlyToJava = " + onlyToJava, Project.MSG_DEBUG);
        log("additionalArgs = ", Project.MSG_DEBUG);
        if (additionalArgs != null) {
            for (Iterator i = additionalArgs.iterator(); i.hasNext(); ) {
                StringBuffer sb = new StringBuffer();
                Commandline.Argument arg = (Commandline.Argument)i.next();
                String[] parts = arg.getParts();
                sb.append("   '");
                for (int j = 0; j < parts.length; j++) {
                    sb.append(parts[j]);
                }
                sb.append("'");
                log(sb.toString(), Project.MSG_DEBUG);
            }            
        }
        log("compileClasspath = " + compileClasspath, Project.MSG_DEBUG);
        log("compileSourcepath = " + compileSourcepath, Project.MSG_DEBUG);
        log("fileset = " + Arrays.asList(this.getCompileFiles()), Project.MSG_DEBUG);

    }
    /**
     * Adds a command-line argument.
     */
    public Commandline.Argument createArg() {
        Commandline.Argument argument = new Commandline.Argument();
        if (additionalArgs == null) {
            additionalArgs = new LinkedList();
        }
        additionalArgs.add(argument);
        return argument;
    }

    public void setExtname(String extensionName) {
        this.extensionName = extensionName;
    }

    public void setExtclass(Class extensionClass) {
        this.extensionClass = extensionClass;
    }

    public void setDestdir(File destdir) {
        this.destDir = destdir;
    }

    public Path createSrc() {
        if (src == null) {
            src = new Path(getProject());
        }
        return src.createPath();
    }
    public void setSrcdir(Path srcDir) {
        if (src == null) {
            src = srcDir;
        } else {
            src.append(srcDir);
        }
    }

    public void setBootclasspath(Path bootclasspath) {
        if (this.bootclasspath == null) {
            this.bootclasspath = bootclasspath;
        } else {
            this.bootclasspath.append(bootclasspath);
        }
    }
    
    public Path createBootclasspath() {
        if (bootclasspath == null) {
            bootclasspath = new Path(getProject());
        }
        return bootclasspath.createPath();
    }
    
    /**
     * Adds a reference to a classpath defined elsewhere.
     */
    public void setBootClasspathRef(Reference r) {
        createBootclasspath().setRefid(r);
    }

    public void setSx(String srcExt) {
        this.srcExt = srcExt;
    }

    public void setOx(String outExt) {
        this.outputExt = outExt;
    }
    public void setOptions(File optionsFile) {
        this.optionsFile = optionsFile;
    }

    public void setPost(String post) {
        this.post = post;
    }

    public void setNoserial(boolean noSerial) {
        this.noSerial = noSerial;
    }

    public void setNooutput(boolean noOutput) {
        this.noOutput = noOutput;
    }

    public void setFqcn(boolean fqcn) {
        this.fqcn = fqcn;
    }

    public void setOnlytojava(boolean onlyToJava) {
        this.onlyToJava = onlyToJava;
    }
    
    public void setClasspath(Path classpath) {
        if (compileClasspath == null) {
            compileClasspath = classpath;
        } else {
            compileClasspath.append(classpath) ; 
        }
    }

    /**
     * Adds a path to the classpath.
     */
    public Path createClasspath() {
        if (compileClasspath == null) {
            compileClasspath = new Path(getProject());
        }
        return compileClasspath.createPath();
    }

    /**
     * Adds a reference to a classpath defined elsewhere.
     */
    public void setClasspathRef(Reference r) {
        createClasspath().setRefid(r);
    }

    /**
     * Set the sourcepath to be used for this compilation.
     */
    public void setSourcepath(Path sourcepath) {
        if (compileSourcepath == null) {
            compileSourcepath = sourcepath;
        } else {
            compileSourcepath.append(sourcepath);
        }
    }

    /** Gets the sourcepath to be used for this compilation. */
    public Path getSourcepath() {
        return compileSourcepath;
    }

    /**
     * Adds a path to sourcepath.
     */
    public Path createSourcepath() {
        if (compileSourcepath == null) {
            compileSourcepath = new Path(getProject());
        }
        return compileSourcepath.createPath();
    }

    /**
     * Adds a reference to a source path defined elsewhere.
     */
    public void setSourcepathRef(Reference r) {
        createSourcepath().setRefid(r);
    }

}
