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

package org.aspectj.tools.ant.taskdefs;

import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.Vector;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Location;
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.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessageHolder;
import org.aspectj.bridge.MessageHandler;
import org.aspectj.tools.ajc.Main;
import org.aspectj.tools.ajc.Main.MessagePrinter;
import org.aspectj.util.FileUtil;

/**
 * Main taskdef class for the AspectJ 1.0 compiler, <code>ajc</code>.
 * See the README and examples/build.xml for more information.
 */
public class Ajc10 extends MatchingTask {

    private static final List<String> VALID_XOPTIONS;
    static {
        String[] xs = new String[] 
            {   "lint", "serializableAspects", "targetNearSource", "OcodeSize",
                "incrementalFile" };
        VALID_XOPTIONS = Collections.unmodifiableList(Arrays.asList(xs));
    }
    //protected boolean version;
    protected boolean source14;
    protected Set ignoredOptions;
    protected Commandline cmd;
    protected Commandline vmcmd;
    private int threads = -1;
    private File destdir;
    private File workingdir;
    private Path internalclasspath;
    private Path classpath;
    private Path bootclasspath;
    private Path extdirs;
    private Path srcdir;
    private List argfiles;
    private boolean fork;
    private boolean failonerror;
    private boolean verbose;
	private String encoding;
	private String source;

    public Ajc10() {
        reset();
    }

    // ------------------------- options 
    // find option types and whether ignored:
    //   sed -n '/void set/p' Ajc.java | sed 's/.*\/\/ //' | sort -u
    //   sed -n '/ignoredOptions/d;/ignored/p' Ajc.java 
    // each option may be "ignored" and is one+ of:
    //   ajc-old             used to be an ajc option
    //   ajc-only            only an option for ajc, not javac
    //   ajc-taskdef only    only an option for ajc taskdef, not ajc
    //   javac-also          also an option in javac
    //   eajc-also

    // ------------------------- options in order per ajc output

    public void setVerbose(boolean verbose) {  // javac-also eajc-also docDone
        setif(verbose, "-verbose");
        this.verbose = verbose;
    }

    public void setVersion(boolean version) {  // javac-also eajc-also docDone
        // let the compiler handle it
        if (version) {
            setif(true, "-version");
        }
        //this.version = version;
    }
    
    public void setNocomments(boolean nocomments) { // ajc-only not-eajc docDone
        if (nocomments) {
            ignore("-nocomments");
        }
        //setif(nocomments, "-nocomments");
    }

    public void setEmacssym(boolean input) {  // ajc-only  eajc-also docDone
        setif(input, "-emacssym"); 
    }

    public void setUsejavac(boolean input) {  // ajc-only not-eajc docDone
        if (input) {
            ignore("-usejavac");
        }
        //setif(input, "-usejavac"); 
    }

    public void setPreprocess(boolean preprocess) { // ajc-only not-eajc docDone
        if (preprocess) {
            ignore("-preprocess");
            //setif(preprocess, "-preprocess");
        }
    }

    public void setWorkingdir(String workingdir) { // ajc-only not-eajc ocDone
        ignore("-workingdir");
        //this.workingdir = project.resolveFile(workingdir);
    }

    public void setDestdir(String destdir) { // javac-also eajc-also  docDone
        this.destdir = project.resolveFile(destdir);
    }

    public void setOptimize(boolean optimize) { // javac-also ignored docDone
        setif(optimize, "-O");
    }
   

    public void setClasspath(Path classpath) {  // javac-also eajc-also docDone
        if (this.classpath == null) {
            this.classpath = classpath;
        } else {
            this.classpath.append(classpath);
        }
    }
    public Path createClasspath() {  // javac-also docDone
        if (classpath == null) {
            classpath = new Path(project);
        }
        return classpath.createPath();
    }        
    
    public void setClasspathref(Reference classpathref) {  // javac-also docDone
        createClasspath().setRefid(classpathref);
    }
    
    public void setBootclasspath(Path bootclasspath) {  // javac-also not-eajc docDone
        ignore("bootclasspath"); // XXX may jury-rig
//        if (this.bootclasspath == null) {
//            this.bootclasspath = bootclasspath;
//        } else {
//            this.bootclasspath.append(bootclasspath);
//        }
    }
    public Path createBootclasspath() {  // javac-also not-eajc docDone
        ignore("bootclasspath"); // XXX may jury-rig
        if (bootclasspath == null) {
            bootclasspath = new Path(project);
        }
        return bootclasspath.createPath();
    }    
    
    public void setBootclasspathref(Reference bootclasspathref) {  // javac-also not-eajc docDone
        ignore("bootclasspath"); // XXX may jury-rig
//        createBootclasspath().setRefid(bootclasspathref);
    }
    
    public void setExtdirs(Path extdirs) {  // javac-also not-eajc docDone
        ignore("-extdirs");
//        if (this.extdirs == null) {
//            this.extdirs = extdirs;
//        } else {
//            this.extdirs.append(extdirs);
//        }
    }

    public Path createExtdirs() {  // javac-also not-eajc docDone
        ignore("-extdirs");
        if (extdirs == null) {
            extdirs = new Path(project);
        }
        return extdirs.createPath();
    }  

    public void setArgfile(File argfile) { // ajc-only eajc-also docDone
        if (argfiles == null) {
            argfiles = new Vector();
        }
        argfiles.add(argfile);
    }

    public void setArgfiles(String argfiles) { // ajc-only eajc-also docDone
        StringTokenizer tok = new StringTokenizer(argfiles, ", ", false);
        if (tok.hasMoreTokens() && this.argfiles == null) {
            this.argfiles = new Vector();
        }
        while (tok.hasMoreTokens()) {
            this.argfiles.add(project.resolveFile(tok.nextToken().trim()));
        }
    }

    public void setEncoding(String encoding) { // ignored eajc-also docDone
        // XXX add support
        //ignore("encoding");
        this.encoding = encoding;
    }

    public void setSource(String input) {    // javac-also (Ant 1.4) eajc-also docDone
        source = input;
        //source14 = "1.4".equals(input); // XXX todo
    }

    public void setLenient(boolean input) {  // ajc-only not-eajc docDone
        ignore("-lenient");
        //setif(input, "-lenient"); 
    }

    public void setStrict(boolean input) {  // ajc-only not-eajc docDone
        ignore("-strict");
        //setif(input, "-strict"); 
    }

    public void setPorting(boolean input) {  // ajc-only not-eajc docDone
        ignore("-porting");
        //setif(input, "-porting"); 
    }

    public void setX(String input) {  // ajc-only eajc-also docDone
        StringTokenizer tokens = new StringTokenizer(input, ",", false);
        while (tokens.hasMoreTokens()) {
            String token = tokens.nextToken().trim();
            if (1 < token.length()) {
                if (VALID_XOPTIONS.contains(token)) {
                    setif(true, "-X" + token); 
                } else {
                    ignore("-X" + token);
                }
            }
        }
    }

    // ------------------------- vestigial
    public void setThreads(int threads) { // ajc-old docDone ignored
        ignore("-threads");
        //this.threads = threads;
    }
    
    public void setDumpstack(boolean dumpstack) { // ajc-old
        ignore("-dumpstack");
        //setif(dumpstack, "-dumpstack");
    }

    // ------------------------- specific to taskdef

    public void setInternalclasspath(Path internalclasspath) { // ajc-taskdef only
        if (this.internalclasspath == null) {
            this.internalclasspath = internalclasspath;
        } else {
            this.internalclasspath.append(internalclasspath);
        }
    }
    public Path createInternalclasspath() { // ajc-taskdef only
        if (internalclasspath == null) {
            internalclasspath = new Path(project);
        }
        return internalclasspath.createPath();
    }        
    
    public void setInternalclasspathref(Reference internalclasspathref) { // ajc-taskdef only
        createInternalclasspath().setRefid(internalclasspathref);
    }

     public void setSrcdir(Path srcdir) { // javac-also eajc-also docDone
        if (this.srcdir == null) {
            this.srcdir = srcdir;
        } else {
            this.srcdir.append(srcdir);
        }
    }

    public Path createSrc() { // javac-also eajc-also docDone
        return createSrcdir();
    }

    public Path createSrcdir() { // javac-also eajc-also docDone
        if (srcdir == null) {
            srcdir = new Path(project);
        }
        return srcdir.createPath();
    }

    public void setFork(boolean fork) { // ajc-only not-eajc docDone
        ignore("fork");
        //this.fork = fork;
    }

    public void setFailonerror(boolean failonerror) {  // javac-also docDone
        this.failonerror = failonerror;
    }

    public void setMaxmemory(String max) { // ajc-taskdef only docDone
        ignore("-maxmemory");
        // we do not run under 1.1 anyway...
//        createJvmarg().setValue((Project.getJavaVersion().
//                                 startsWith("1.1") ?
//                                 "-mx" : "-Xmx") +max);
    }

    public void setJvmarg(String input) {  // ajc-taskdef only docDone
        ignore("jvmarg"); // XXX fork
        //vmcmd.createArgument().setValue(input);
    }

    public Commandline.Argument createJvmarg() { // ajc-taskdef only docDone
        ignore("jvmarg"); // XXX fork
        return vmcmd.createArgument();
    }

    // ------------------------- javac task compatibility
    public void setNosymbols(boolean nosymbols) { // todo remove?
        ignore("-nosymbols");
        //setif(nosymbols, "-nosymbols");
    }
    
    public void setDebug(boolean debug) { // javac-also eajc-also docDone
        setif(debug, "-g"); // todo: changed from -debug
    }
    
    public void setDeprecation(boolean deprecation) { // javac-also eajc-also docDone
        setif(deprecation, "-deprecation"); // XXX eajc: also "warn:deprecation"
    }

    // ------------------------- javac task compatibility - ignored
    public void setTarget(String target) {  // javac-also ignored docDone
        ignore("target"); // todo: ajc accepts but doesn't use - pass in?
    }
    public void setDepend(String depend) {  // javac-also ignored docDone
        ignore("depend");
    }
    public void setIncludeantruntime(boolean includeAntruntime) {  // javac-also ignored docDone
        ignore("includeantruntime");
    }
    public void setIncludejavaruntime(boolean includeJavaruntime ) {  // javac-also ignored docDone
        ignore("includeJavaruntime");
    }

    // ------------------------- other state methods
    
    protected final void ignore(String attribute) {
        ignoredOptions.add(attribute);
    }

    public void backdoorSetFile(File file) {
        if (null != file) {
            cmd.createArgument().setFile(file);
        }
    }
    
    /** reset variables to permit gc */
    public void reset() {
        //version = false;
        source14 = false;
        ignoredOptions = new HashSet();
        cmd = new Commandline();
        vmcmd = new Commandline();
        threads = -1;
        destdir = null;
        workingdir = null;
        internalclasspath = null;
        classpath = null;
        bootclasspath = null;
        extdirs = null;
        srcdir = null;
        argfiles = null;
        fork = false;
        failonerror = true;
        encoding = null;
        source = null;
    }

    protected final void setif(boolean b, String flag) {
        if (b) cmd.createArgument().setValue(flag);
    }

    // ------------------------- operational methods

    @Override
	public void execute() throws BuildException {
        if (srcdir == null && argfiles == null) {
            throw new BuildException("one of srcdir or argfiles must be set!",
                                     location);
        }
//        if (threads != -1) {
//            cmd.createArgument().setValue("-threads");
//            cmd.createArgument().setValue(threads+"");
//        }
//        if (workingdir != null) {
//            cmd.createArgument().setValue("-workingdir");
//            cmd.createArgument().setFile(workingdir);
//        }
        if (destdir != null) {
            cmd.createArgument().setValue("-d");
            cmd.createArgument().setFile(destdir);
        }
        if (classpath != null) {
            cmd.createArgument().setValue("-classpath");
            cmd.createArgument().setPath(classpath);
        }
        if (bootclasspath != null) {
            cmd.createArgument().setValue("-bootclasspath");
            cmd.createArgument().setPath(bootclasspath);
        }
        if (extdirs != null) {
            cmd.createArgument().setValue("-extdirs");
            cmd.createArgument().setPath(extdirs);
        }
        if (null != encoding) {
            cmd.createArgument().setValue("-encoding");
            cmd.createArgument().setValue(encoding);
        }
        if (null != source) {
            cmd.createArgument().setValue("-source");
            cmd.createArgument().setValue(source);
        }

        int numargfiles = 0;
        if (argfiles != null) {
            for (Iterator i = argfiles.iterator(); i.hasNext();) {
                String name = i.next()+"";
                File argfile = project.resolveFile(name);
                if (check(argfile, name, false, location)) {
                    cmd.createArgument().setValue("-argfile");
                    cmd.createArgument().setFile(argfile);
                    numargfiles++;    
                }
            }
        }
        int numfiles = 0;
        if (srcdir != null) {
            // todo: ignore any srcdir if any argfiles and no explicit includes
            String[] dirs = srcdir.list();
            for (int i = 0; i < dirs.length; i++) {
                File dir = project.resolveFile(dirs[i]);
                check(dir, dirs[i], true, location);
                String[] files = getDirectoryScanner(dir).getIncludedFiles();
                for (int j = 0; j < files.length; j++) {
                    File file = new File(dir, files[j]);
                    if (FileUtil.hasSourceSuffix(file)) {
                        cmd.createArgument().setFile(file);
                        numfiles++;
                    }
                }
            }
        }
        if ((null != ignoredOptions) && (ignoredOptions.size() > 0)) {
            log("The following attributes were ignored " + ignoredOptions,
                Project.MSG_WARN);
            if (ignoredOptions.contains("-preprocess")) {
                throw new BuildException("preprocess no longer supported");
            }
        }
        log("Compiling " + numfiles + " source and " +
            + numargfiles + " arg files" 
            + (null == destdir ? "" : " to " + destdir.getPath()), Project.MSG_INFO);

         // here is the actual invocation
        //int result = (fork || (internalclasspath != null)) ? fork() : spoon();
        if (fork || (internalclasspath != null))  {
        	log("WARNING: fork not supported", Project.MSG_WARN);
        }
        int result = spoon();
        if (result != 0) {
            String msg = "Compilation error: " + result;
            if (failonerror) {
                reset();
                throw new BuildException(msg);
            } else {
                log(msg, Project.MSG_WARN);
            }
        }
        reset(); // see throw above
    }

// now leaving version to compiler - remove
//    protected void version(Path classpath) {
//        try {
//            Class main = findClass("org.aspectj.tools.ajc.Main",
//                                   classpath);
//            Method printVersion = main.getDeclaredMethod("printVersion",
//                                                         new Class[]{});
//            printVersion.setAccessible(true);
//            printVersion.invoke(main.newInstance(), new Object[]{});
//        } catch (Exception e) {}
//    }
//
//    protected Class findClass(String classname, Path classpathPath) {
//        String classpath = (classpathPath != null ?
//                            classpathPath+"" : "").trim();
//        if (classpath.length() == 0) {
//            try {
//                return Class.forName(classname);
//            } catch (ClassNotFoundException e){}
//        }
//        List urls = new ArrayList();
//        for (StringTokenizer t = new StringTokenizer(classpath,
//                                                     File.pathSeparator);
//             t.hasMoreTokens();) {
//            File f = new File(t.nextToken().trim());
//            try {
//                if (f.exists()) {
//                    URL url = f.toURL();
//                    if (url != null) urls.add(url);
//                }
//            } catch (MalformedURLException e) {}
//        }
//        if (urls.size() == 0) return null;
//        try {
//            return new URLClassLoader((URL[])urls.toArray
//                                      (new URL[urls.size()]),
//                                      null).loadClass(classname);
//        } catch (ClassNotFoundException e) {}
//        return null;
//    }

// unused now, but leave in
//    protected int fork() throws BuildException {
//        Java java = (Java)project.createTask("java");
//        java.setTaskName(getTaskName());
//        Path compileClasspath;
//        if (internalclasspath != null) {
//            compileClasspath = internalclasspath;
//            compileClasspath.append(Path.systemClasspath);
//        } else {
//            compileClasspath = Path.systemClasspath;
//        }
//        //if (version) version(compileClasspath);
//        java.setClasspath(compileClasspath);
//        java.setClassname(FALSE_MAIN);
//        String[] args;
//        args = cmd.getArguments();
//        for (int i = 0; i < args.length; i++) {
//            java.createArg().setValue(args[i]);
//        }
//        args = vmcmd.getArguments();
//        for (int i = 0; i < args.length; i++) {
//            java.createJvmarg().setValue(args[i]);
//        }
//        java.setFork(fork);
//        // java handles its own verbose logging when forking
//        return java.executeJava();
//    }

    /** utility to render String[] for logging */
    public static String render(String[] args) {
        if (null == args) return "";
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < args.length; i++) {
            sb.append(args[i]);
            sb.append(" ");
        }
        return sb.toString();
    }

    protected int spoon() throws BuildException {
        //if (version) version(null);
        int result = -1;
        final IMessageHolder holder;
        {
	    	MessageHandler handler = new MessageHandler();
	    	if (!verbose) {
	  			handler.ignore(IMessage.INFO);
	  		}
	    	final IMessageHandler delegate 
	    		= verbose ? MessagePrinter.VERBOSE: MessagePrinter.TERSE;
			handler.setInterceptor(delegate);
			holder = handler;
        }
        try {
            String[] args = cmd.getCommandline();
            // XXX avoid rendering if not verbosely logging?
            log("Running  in-process using " 
                + Ajc10.render(cmd.getCommandline()), Project.MSG_VERBOSE);
        	
        	Main main = new Main();
        	main.run(args, holder);
        	int errs = holder.numMessages(IMessage.ERROR, true);
        	if (0 < errs) {
        		result = 1;
        	} else {
                result = 0;
            }
        } catch (Throwable t) {
            while (t instanceof AbortException) {
                // check for "just quit -- message printed already"
                if (((AbortException)t).isSilent()) { 
                    t = null;
                    break;
                }
                IMessage m = ((AbortException) t).getIMessage();
                if (null == m) {
                    break;
                } else {
                    Throwable tt = m.getThrown();
                    if (null != tt) {
                        t = tt;   
                    } else {
                        break;
                    }
                }
            }
            if (null != t) {
                // t.printStackTrace(); // let recipient print
                throw new BuildException("Compiler failure", t, location);
            }
        } finally {
        	// now printing messages as we go, above
//			IMessage.Kind level = (verbose ? IMessage.INFO : IMessage.WARNING);
//        	if (0 < holder.numMessages(level, true)) {
//        		final String prefix = "";
//        		final boolean printSummary = false;
//        		MessageUtil.print(System.err, 
//        			holder, 
//        			prefix, 
//        			MessageUtil.MESSAGE_ALL,
//        			(verbose ? MessageUtil.PICK_INFO_PLUS : MessageUtil.PICK_WARNING_PLUS),
//        			printSummary);
//        	}
        }
        return result;
    }

    protected final boolean check(File file, String name,
                                  boolean isDir, Location loc) {
        loc = loc != null ? loc : location;
        if (file == null) {
            throw new BuildException(name + " is null!", loc);
        }
        if (!file.exists()) {
            throw new BuildException(file + "doesn't exist!", loc);
        }
        if (isDir ^ file.isDirectory()) {
            String e = file + " should" + (isDir ? "" : "n't")  +
                " be a directory!";
            throw new BuildException(e, loc);
        }
        return true;
    }
}
