/*
 * The Apache Software License, Version 1.1
 *
 * Copyright (c) 1999 The Apache Software Foundation.  All rights
 * reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 *
 * 3. The end-user documentation included with the redistribution, if
 *    any, must include the following acknowlegement:
 *       "This product includes software developed by the
 *        Apache Software Foundation (http://www.apache.org/)."
 *    Alternately, this acknowlegement may appear in the software itself,
 *    if and wherever such third-party acknowlegements normally appear.
 *
 * 4. The names "The Jakarta Project", "Tomcat", and "Apache Software
 *    Foundation" must not be used to endorse or promote products derived
 *    from this software without prior written permission. For written
 *    permission, please contact apache@apache.org.
 *
 * 5. Products derived from this software may not be called "Apache"
 *    nor may "Apache" appear in their names without prior written
 *    permission of the Apache Group.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED.  IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR
 * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 * SUCH DAMAGE.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 */

package au.com.multitask.ant.taskdefs;

import org.apache.tools.ant.*;
import org.apache.tools.ant.taskdefs.*;

import java.io.*;
import java.lang.reflect.*;
import java.util.*;

/**
 * Task to compile NetRexx source files. This task can take the following
 * arguments:
 * <ul>
 * <li>binary</li>
 * <li>classpath</li>
 * <li>comments</li>
 * <li>compile</li>
 * <li>console</li>
 * <li>crossref</li>
 * <li>decimal</li>
 * <li>destdir</li>
 * <li>diag</li>
 * <li>explicit</li>
 * <li>format</li>
 * <li>keep</li>
 * <li>logo</li>
 * <li>replace</li>
 * <li>savelog</li>
 * <li>srcdir</li>
 * <li>sourcedir</li>
 * <li>strictargs</li>
 * <li>strictassign</li>
 * <li>strictcase</li>
 * <li>strictimport</li>
 * <li>symbols</li>
 * <li>time</li>
 * <li>trace</li>
 * <li>utf8</li>
 * <li>verbose</li>
 * </ul>
 * Of these arguments, the <b>srcdir</b> argument is required.
 *
 * <p>When this task executes, it will recursively scan the srcdir
 * looking for NetRexx source files to compile. This task makes its
 * compile decision based on timestamp.
 * <p>Before files are compiled they and any other file in the
 * srcdir will be copied to the destdir allowing support files to be
 * located properly in the classpath. The reason for copying the source files
 * before the compile is that NetRexxC has only two destinations for classfiles:
 * <ol>
 * <li>The current directory, and,</li>
 * <li>The directory the source is in (see sourcedir option)
 * </ol>
 *
 * @author dIon Gillard <a href="mailto:dion@multitask.com.au">dion@multitask.com.au</a>
 */

public class NetRexxC extends MatchingTask {

    // variables to hold arguments
    private boolean binary;
    private String classpath;
    private boolean comments;
    private boolean compact;
    private boolean compile = true;
    private boolean console;
    private boolean crossref;
    private boolean decimal = true;
    private File destDir;
    private boolean diag;
    private boolean explicit;
    private boolean format;
    private boolean java;
    private boolean keep;
    private boolean logo = true;
    private boolean replace;
    private boolean savelog;
    private File srcDir;
    private boolean sourcedir = true; // ?? Should this be the default for ant?
    private boolean strictargs;
    private boolean strictassign;
    private boolean strictcase;
    private boolean strictimport;
    private boolean strictprops;
    private boolean strictsignal;
    private boolean symbols;
    private boolean time;
    private String trace = "trace2";
    private boolean utf8;
    private String verbose = "verbose3";

    // other implementation variables
    private Vector compileList = new Vector();
    private Hashtable filecopyList = new Hashtable();
    private String oldClasspath = System.getProperty("java.class.path");
    private String compilerClass = "COM.ibm.netrexx.process.NetRexxC";


    /**
     * Set whether literals are treated as binary, rather than NetRexx types
     */
    public void setBinary(String binary) {
        this.binary = Project.toBoolean(binary);
    }

    /**
     * Set the classpath used for NetRexx compilation
     */
    public void setClasspath(String classpath) {
        this.classpath = classpath;
    }

    /**
     * Set whether comments are passed through to the generated java source.
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false
     */
    public void setComments(String comments) {
        this.comments = Project.toBoolean(comments);
    }

   /**
     * Set whether error messages come out in compact or verbose format.
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false
     */
    public void setCompact(String compact) {
        this.compact = Project.toBoolean(compact);
    }

    /**
     * Set whether the NetRexx compiler should compile the generated java code
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is true.
     * Setting this flag to false, will automatically set the keep flag to true.
     */
    public void setCompile(String compile) {
        this.compile = Project.toBoolean(compile);
        if (!this.compile && !this.keep) this.keep = true;
    }

    /**
     * Set whether or not messages should be displayed on the 'console'
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is true.
     */
    public void setConsole(String console) {
        this.console = Project.toBoolean(console);
    }

    /**
     * Whether variable cross references are generated
     */
    public void setCrossref(String crossref) {
        this.crossref = Project.toBoolean(crossref);
    }

    /**
     * Set whether decimal arithmetic should be used for the netrexx code.
     * Binary arithmetic is used when this flag is turned off.
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is true.
     */
    public void setDecimal(String decimal) {
        this.decimal = Project.toBoolean(decimal);
    }

    /**
     * Set the destination directory into which the NetRexx source
     * files should be copied and then compiled.
     */
    public void setDestDir(String destDirName) {
        destDir = project.resolveFile(destDirName);
    }

    /**
     * Whether diagnostic information about the compile is generated
     */
    public void setDiag(String diag) {
        this.diag = Project.toBoolean(diag);
    }

    /**
     * Sets whether variables must be declared explicitly before use.
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setExplicit(String explicit) {
        this.explicit = Project.toBoolean(explicit);
    }

    /**
     * Whether the generated java code is formatted nicely or left to match NetRexx
     * line numbers for call stack debugging
     */
    public void setFormat(String format) {
        this.format = Project.toBoolean(format);
    }

    /**
     * Whether the generated java code is produced
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setJava(String java) {
        this.java = Project.toBoolean(java);
    }


    /**
     * Sets whether the generated java source file should be kept after compilation.
     * The generated files will have an extension of .java.keep, <b>not</b> .java
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setKeep(String keep) {
        this.keep = Project.toBoolean(keep);
    }

    /**
     * Whether the compiler text logo is displayed when compiling
     */
    public void setLogo(String logo) {
        this.logo = Project.toBoolean(logo);
    }

     /**
     * Whether the generated .java file should be replaced when compiling
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setReplace(String replace) {
        this.replace = Project.toBoolean(replace);
    }

    /**
     * Sets whether the compiler messages will be written to NetRexxC.log as
     * well as to the console
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setSavelog(String savelog) {
        this.savelog = Project.toBoolean(savelog);
    }

    /**
     * Tells the NetRexx compiler to store the class files in the same directory
     * as the source files. The alternative is the working directory
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is true.
     */
    public void setSourcedir(String sourcedir) {
        this.sourcedir = Project.toBoolean(sourcedir);
    }

    /**
     * Set the source dir to find the source Java files.
     */
    public void setSrcDir(String srcDirName) {
        srcDir = project.resolveFile(srcDirName);
    }

    /**
     * Tells the NetRexx compiler that method calls always need parentheses,
     * even if no arguments are needed, e.g. <code>aStringVar.getBytes</code>
     * vs. <code>aStringVar.getBytes()</code>
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setStrictargs(String strictargs) {
        this.strictargs = Project.toBoolean(strictargs);
    }

    /**
     * Tells the NetRexx compile that assignments must match exactly on type
     */
    public void setStrictassign(String strictassign) {
        this.strictassign = Project.toBoolean(strictassign);
    }

    /**
     * Specifies whether the NetRexx compiler should be case sensitive or not
     */
    public void setStrictcase(String strictcase) {
        this.strictcase = Project.toBoolean(strictcase);
    }

    /**
     * Sets whether classes need to be imported explicitly using an
     * <code>import</code> statement. By default the NetRexx compiler will import
     * certain packages automatically
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setStrictimport(String strictimport) {
        this.strictimport = Project.toBoolean(strictimport);
    }

    /**
     * Sets whether local properties need to be qualified explicitly using <code>this</code>
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setStrictprops(String strictprops) {
        this.strictprops = Project.toBoolean(strictprops);
    }


    /**
     * Whether the compiler should force catching of exceptions by explicitly named types
     */
    public void setStrictsignal(String strictsignal) {
        this.strictsignal = Project.toBoolean(strictsignal);
    }

    /**
     * Sets whether debug symbols should be generated into the class file
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setSymbols(String symbols) {
        this.symbols = Project.toBoolean(symbols);
    }

    /**
     * Asks the NetRexx compiler to print compilation times to the console
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setTime(String time) {
        this.time = Project.toBoolean(time);
    }

    /**
     * Turns on or off tracing and directs the resultant trace output
     * Valid values are: "trace", "trace1", "trace2" and "notrace".
     * "trace" and "trace2"
     */
    public void setTrace(String trace) {
        if (trace.equalsIgnoreCase("trace")
        || trace.equalsIgnoreCase("trace1")
        || trace.equalsIgnoreCase("trace2")
        || trace.equalsIgnoreCase("notrace")) {
            this.trace = trace;
        } else {
            throw new BuildException("Unknown trace value specified: '" + trace + "'");
        }
    }

    /**
     * Tells the NetRexx compiler that the source is in UTF8
     * Valid true values are "on" or "true". Anything else sets the flag to false.
     * The default value is false.
     */
    public void setUtf8(String utf8) {
        this.utf8 = Project.toBoolean(utf8);
    }

    /**
     * Whether lots of warnings and error messages should be generated
     */
    public void setVerbose(String verbose) {
        this.verbose = verbose;
    }

    /**
     * Executes the task, i.e. does the actual compiler call
     */
    public void execute() throws BuildException {

        // first off, make sure that we've got a srcdir and destdir
        if (srcDir == null || destDir == null ) {
            throw new BuildException("srcDir and destDir attributes must be set!");
        }

        // scan source and dest dirs to build up both copy lists and
        // compile lists
        //        scanDir(srcDir, destDir);
        DirectoryScanner ds = getDirectoryScanner(srcDir);

        String[] files = ds.getIncludedFiles();

        scanDir(srcDir, destDir, files);

        // copy the source and support files
        copyFilesToDestination();

        // compile the source files
        if (compileList.size() > 0) {
            project.log("Compiling " + compileList.size() + " source files to " + destDir);
            doNetRexxCompile();
        }
    }

    /**
     * Scans the directory looking for source files to be compiled and
     * support files to be copied.
     */
    private void scanDir(File srcDir, File destDir, String[] files) {
        for (int i = 0; i < files.length; i++) {
            File srcFile = new File(srcDir, files[i]);
            File destFile = new File(destDir, files[i]);
            String filename = files[i];
            // if it's a non source file, copy it if a later date than the
            // dest
            // if it's a source file, see if the destination class file
            // needs to be recreated via compilation
            if (filename.toLowerCase().endsWith(".nrx")) {
                File classFile = new File(destDir, filename.substring(0, filename.lastIndexOf('.')) + ".class");
                if (!compile || srcFile.lastModified() > classFile.lastModified()) {
                    filecopyList.put(srcFile.getAbsolutePath(), destFile.getAbsolutePath());
                    compileList.addElement(destFile.getAbsolutePath());
                }
            } else {
                if (srcFile.lastModified() > destFile.lastModified()) {
                    filecopyList.put(srcFile.getAbsolutePath(), destFile.getAbsolutePath());
                }
            }
        }
    }

    /**
     * Copy eligible files from the srcDir to destDir
     */
    private void copyFilesToDestination() {
        if (filecopyList.size() > 0) {
            project.log("Copying " + filecopyList.size() + " files to " + destDir.getAbsolutePath());
            Enumeration enum = filecopyList.keys();
            while (enum.hasMoreElements()) {
                String fromFile = (String)enum.nextElement();
                String toFile = (String)filecopyList.get(fromFile);
                try {
                    project.copyFile(fromFile, toFile);
                } catch (IOException ioe) {
                    String msg = "Failed to copy " + fromFile + " to " + toFile
                    + " due to " + ioe.getMessage();
                    throw new BuildException(msg, ioe);
                }
            }
        }
    }

    /**
     * Peforms a copmile using the NetRexx 1.1.x compiler
     */
    private void doNetRexxCompile() throws BuildException {
        project.log("Using NetRexx compiler", project.MSG_VERBOSE);
        String classpath = getCompileClasspath();
        StringBuffer compileOptions = new StringBuffer();
        StringBuffer fileList = new StringBuffer();

        // create an array of strings for input to the compiler: one array
        // comes from the compile options, the other from the compileList
        String[] compileOptionsArray = getCompileOptionsAsArray();
        String[] fileListArray = new String[compileList.size()];
        Enumeration e = compileList.elements();
        int j = 0;
        while (e.hasMoreElements()) {
            fileListArray[j] = (String)e.nextElement();
            j++;
        }
        // create a single array of arguments for the compiler
        String compileArgs[] = new String[compileOptionsArray.length + fileListArray.length];
        for (int i = 0; i < compileOptionsArray.length; i++) {
            compileArgs[i] = compileOptionsArray[i];
        }
        for (int i = 0; i < fileListArray.length; i++) {
            compileArgs[i+compileOptionsArray.length] = fileListArray[i];
        }

        // print nice output about what we are doing for the log
        compileOptions.append("Compilation args: ");
        for (int i = 0; i < compileOptionsArray.length; i++) {
            compileOptions.append(compileOptionsArray[i]);
            compileOptions.append(" ");
        }
        project.log(compileOptions.toString(), project.MSG_VERBOSE);

        String eol = System.getProperty("line.separator");
        StringBuffer niceSourceList = new StringBuffer("Files to be compiled:" + eol);

        for (int i = 0; i < compileList.size(); i++) {
            niceSourceList.append("    ");
            niceSourceList.append(compileList.elementAt(i).toString());
            niceSourceList.append(eol);
        }

        project.log(niceSourceList.toString(), project.MSG_VERBOSE);

        // need to set java.class.path property and restore it later
        // since the NetRexx compiler has no option for the classpath
        String currentClassPath = System.getProperty("java.class.path");
        Properties currentProperties = System.getProperties();
        currentProperties.put("java.class.path", classpath);
        try {
            Class compiler = Class.forName(compilerClass);
            Class rexxClass = Class.forName("netrexx.lang.Rexx");
        Constructor rexxConstructor = rexxClass.getDeclaredConstructor(new Class[] {String[].class});
        Object rexxCompileArgs = rexxConstructor.newInstance(new Object[] {compileArgs});
            Method compileMethod = compiler.getDeclaredMethod("main",
        new Class[] {rexxClass, PrintWriter.class});
            Integer rc = (Integer)compileMethod.invoke(compiler, new Object[] {
            rexxCompileArgs, new PrintWriter(project.getOutput())});
            if (rc.intValue() > 1) { // 1 is warnings from real NetRexxC\
                String msg = "Compile failed, messages should have been provided.";
                throw new BuildException(msg);
            }
        } catch (ClassNotFoundException cnfe) {
            throw new BuildException("NetRexx compiler :'" + compilerClass + "' not found: ", cnfe);
        } catch (NoSuchMethodException nsme) {
            throw new BuildException("NetRexx compiler 'main' method not found", nsme);
        } catch (InvocationTargetException ite) {
            throw new BuildException("Exception thrown during compile: " + ite.toString(), ite);
        } catch (IllegalAccessException iae) {
            throw new BuildException("IllegalAccessException during compile: " + iae.toString(), iae);
        } catch (InstantiationException ie) {
            throw new BuildException(ie);
        } finally {
            // need to reset java.class.path property
            // since the NetRexx compiler has no option for the classpath
            currentProperties = System.getProperties();
            currentProperties.put("java.class.path", currentClassPath);
        }
    }

    /**
     * Builds the compilation classpath.
     */
    private String getCompileClasspath() {
        StringBuffer classpath = new StringBuffer();

        // add dest dir to classpath so that previously compiled and
        // untouched classes are on classpath
        classpath.append(destDir.getAbsolutePath());

        // add our classpath to the mix
        if (this.classpath != null) {
            addExistingToClasspath(classpath, this.classpath);
        }

        // add the system classpath
        // addExistingToClasspath(classpath,System.getProperty("java.class.path"));
        return classpath.toString();
    }

    /**
     * This
     */
    private String[] getCompileOptionsAsArray() {
        Vector options = new Vector();
        options.addElement(binary ? "-binary" : "-nobinary");
        options.addElement(comments ? "-comments" : "-nocomments");
        options.addElement(compile ? "-compile" : "-nocompile");
        options.addElement(compact ? "-compact" : "-nocompact");
        options.addElement(console ? "-console" : "-noconsole");
        options.addElement(crossref ? "-crossref" : "-nocrossref");
        options.addElement(decimal ? "-decimal" : "-nodecimal");
        options.addElement(diag ? "-diag" : "-nodiag");
        options.addElement(explicit ? "-explicit": "-noexplicit");
        options.addElement(format ? "-format" : "-noformat");
        options.addElement(keep ? "-keep" : "-nokeep");
        options.addElement(logo ? "-logo" : "-nologo");
        options.addElement(replace ? "-replace" : "-noreplace");
        options.addElement(savelog ? "-savelog" : "-nosavelog");
        options.addElement(sourcedir ? "-sourcedir" : "-nosourcedir");
        options.addElement(strictargs ? "-strictargs" : "-nostrictargs");
        options.addElement(strictassign ? "-strictassign" : "-nostrictassign");
        options.addElement(strictcase ? "-strictcase": "-nostrictcase");
        options.addElement(strictimport ? "-strictimport" : "-nostrictimport");
        options.addElement(strictprops ? "-strictprops" : "-nostrictprops");
        options.addElement(strictsignal ? "-strictsignal" : "-nostrictsignal");
        options.addElement(symbols ? "-symbols" : "-nosymbols");
        options.addElement(time ? "-time" : "-notime");
        options.addElement("-" + trace);
        options.addElement(utf8 ? "-utf8" : "-noutf8");
        options.addElement("-" + verbose);
        String[] results = new String[options.size()];
        options.copyInto(results);
        return results;
    }
    /**
     * Takes a classpath-like string, and adds each element of
     * this string to a new classpath, if the components exist.
     * Components that don't exist, aren't added.
     * We do this, because jikes issues warnings for non-existant
     * files/dirs in his classpath, and these warnings are pretty
     * annoying.
     * @param target - target classpath
     * @param source - source classpath
     * to get file objects.
     */
    private void addExistingToClasspath(StringBuffer target,String source) {
        StringTokenizer tok = new StringTokenizer(source,
        System.getProperty("path.separator"), false);
        while (tok.hasMoreTokens()) {
            File f = project.resolveFile(tok.nextToken());

            if (f.exists()) {
                target.append(File.pathSeparator);
                target.append(f.getAbsolutePath());
            } else {
                project.log("Dropping from classpath: "+
                f.getAbsolutePath(),project.MSG_VERBOSE);
            }
        }

    }
}
