/*
 * IzPack - Copyright 2001-2008 Julien Ponge, All Rights Reserved.
 * 
 * http://izpack.org/
 * http://izpack.codehaus.org/
 * 
 * Copyright 2003 Tino Schwarze
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 *     
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.izforge.izpack.installer;

import com.izforge.izpack.LocaleDatabase;
import com.izforge.izpack.adaptator.IXMLElement;
import com.izforge.izpack.adaptator.IXMLParser;
import com.izforge.izpack.adaptator.impl.XMLParser;
import com.izforge.izpack.util.Debug;
import com.izforge.izpack.util.FileExecutor;
import com.izforge.izpack.util.OsConstraint;
import com.izforge.izpack.util.VariableSubstitutor;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;

/**
 * This class does alle the work for compiling sources.
 * <p/>
 * It responsible for
 * <ul>
 * <li>parsing the compilation spec XML file
 * <li>collecting and creating all jobs
 * <li>doing the actual compilation
 * </ul>
 *
 * @author Tino Schwarze
 */
public class CompileWorker implements Runnable
{

    /**
     * Compilation jobs
     */
    private ArrayList<CompilationJob> jobs;

    /**
     * Name of resource for specifying compilation parameters.
     */
    private static final String SPEC_RESOURCE_NAME = "CompilePanel.Spec.xml";

    private static final String ECLIPSE_COMPILER_NAME = "Integrated Eclipse JDT Compiler";

    private static final String ECLIPSE_COMPILER_CLASS = "org.eclipse.jdt.internal.compiler.batch.Main";

    private VariableSubstitutor vs;

    private IXMLElement spec;

    private AutomatedInstallData idata;

    private CompileHandler handler;

    private IXMLElement compilerSpec;

    private ArrayList<String> compilerList;

    private String compilerToUse;

    private IXMLElement compilerArgumentsSpec;

    private ArrayList<String> compilerArgumentsList;

    private String compilerArgumentsToUse;

    private CompileResult result = null;

    /**
     * The constructor.
     *
     * @param idata   The installation data.
     * @param handler The handler to notify of progress.
     * @throws IOException
     */
    public CompileWorker(AutomatedInstallData idata, CompileHandler handler) throws IOException
    {
        this.idata = idata;
        this.handler = handler;
        this.vs = new VariableSubstitutor(idata.getVariables());
        if (!readSpec())
        {
            throw new IOException("Error reading compilation specification");
        }
    }

    /**
     * Return list of compilers to choose from.
     *
     * @return ArrayList of String
     */
    public ArrayList<String> getAvailableCompilers()
    {
        readChoices(this.compilerSpec, this.compilerList);
        return this.compilerList;
    }

    /**
     * Set the compiler to use.
     * <p/>
     * The compiler is checked before compilation starts.
     *
     * @param compiler compiler to use (not checked)
     */
    public void setCompiler(String compiler)
    {
        this.compilerToUse = compiler;
    }

    /**
     * Get the compiler used.
     *
     * @return the compiler.
     */
    public String getCompiler()
    {
        return this.compilerToUse;
    }

    /**
     * Return list of compiler arguments to choose from.
     *
     * @return ArrayList of String
     */
    public ArrayList<String> getAvailableArguments()
    {
        readChoices(this.compilerArgumentsSpec, this.compilerArgumentsList);
        return this.compilerArgumentsList;
    }

    /**
     * Set the compiler arguments to use.
     *
     * @param arguments The argument to use.
     */
    public void setCompilerArguments(String arguments)
    {
        this.compilerArgumentsToUse = arguments;
    }

    /**
     * Get the compiler arguments used.
     *
     * @return The arguments used for compiling.
     */
    public String getCompilerArguments()
    {
        return this.compilerArgumentsToUse;
    }

    /**
     * Get the result of the compilation.
     *
     * @return The result.
     */
    public CompileResult getResult()
    {
        return this.result;
    }

    /**
     * Start the compilation in a separate thread.
     */
    public void startThread()
    {
        Thread compilationThread = new Thread(this, "compilation thread");
        // will call this.run()
        compilationThread.start();
    }

    /**
     * This is called when the compilation thread is activated.
     * <p/>
     * Can also be called directly if asynchronous processing is not desired.
     */
    public void run()
    {
        try
        {
            if (!collectJobs())
            {
                List<String> args = new ArrayList<String>();
                args.add("nothing to do");

                this.result = new CompileResult(this.idata.langpack
                        .getString("CompilePanel.worker.nofiles"), args, "", "");
            }
            else
            {
                this.result = compileJobs();
            }
        }
        catch (Exception e)
        {
            this.result = new CompileResult(e);
        }

        this.handler.stopAction();
    }

    private boolean readSpec()
    {
        InputStream input;
        try
        {
            input = ResourceManager.getInstance().getInputStream(SPEC_RESOURCE_NAME);
        }
        catch (Exception e)
        {
            e.printStackTrace();
            return false;
        }
        IXMLParser parser = new XMLParser();

        try
        {
            this.spec = (IXMLElement) parser.parse(input);
        }
        catch (Exception e)
        {
            System.out.println("Error parsing XML specification for compilation.");
            e.printStackTrace();
            return false;
        }

        if (!this.spec.hasChildren())
        {
            return false;
        }

        this.compilerArgumentsList = new ArrayList<String>();
        this.compilerList = new ArrayList<String>();

        // read <global> information
        IXMLElement global = this.spec.getFirstChildNamed("global");

        // use some default values if no <global> section found
        if (global != null)
        {

            // get list of compilers
            this.compilerSpec = global.getFirstChildNamed("compiler");

            if (this.compilerSpec != null)
            {
                readChoices(this.compilerSpec, this.compilerList);
            }

            this.compilerArgumentsSpec = global.getFirstChildNamed("arguments");

            if (this.compilerArgumentsSpec != null)
            {
                // basicly perform sanity check
                readChoices(this.compilerArgumentsSpec, this.compilerArgumentsList);
            }

        }

        // supply default values if no useful ones where found
        if (this.compilerList.size() == 0)
        {
            this.compilerList.add("javac");
            this.compilerList.add("jikes");
        }

        if (this.compilerArgumentsList.size() == 0)
        {
            this.compilerArgumentsList.add("-O -g:none");
            this.compilerArgumentsList.add("-O");
            this.compilerArgumentsList.add("-g");
            this.compilerArgumentsList.add("");
        }

        return true;
    }

    // helper function
    private void readChoices(IXMLElement element, ArrayList<String> choiceList)
    {
        Vector<IXMLElement> choices = element.getChildrenNamed("choice");

        if (choices == null)
        {
            return;
        }

        choiceList.clear();

        Iterator<IXMLElement> choice_it = choices.iterator();

        while (choice_it.hasNext())
        {
            IXMLElement choice = choice_it.next();

            String value = choice.getAttribute("value");

            if (value != null)
            {
                List<OsConstraint> osconstraints = OsConstraint.getOsList(choice);

                if (OsConstraint.oneMatchesCurrentSystem(osconstraints))
                {
                    if (value.equalsIgnoreCase(ECLIPSE_COMPILER_NAME))
                    {
                        // check for availability of eclipse compiler
                        try
                        {
                            Class.forName(ECLIPSE_COMPILER_CLASS);
                            choiceList.add(value);
                        }
                        catch (ExceptionInInitializerError eiie)
                        {
                            // ignore, just don't add it as a choice                            
                        }
                        catch (ClassNotFoundException cnfe)
                        {
                            // ignore, just don't add it as a choice
                        }
                    }
                    else
                    {
                        choiceList.add(this.vs.substitute(value, "plain"));
                    }
                }
            }

        }

    }

    /**
     * Parse the compilation specification file and create jobs.
     */
    private boolean collectJobs() throws Exception
    {
        IXMLElement data = this.spec.getFirstChildNamed("jobs");

        if (data == null)
        {
            return false;
        }

        // list of classpath entries
        ArrayList classpath = new ArrayList();

        this.jobs = new ArrayList<CompilationJob>();

        // we throw away the toplevel compilation job
        // (all jobs are collected in this.jobs)
        collectJobsRecursive(data, classpath);

        return true;
    }

    /**
     * perform the actual compilation
     */
    private CompileResult compileJobs()
    {
        ArrayList<String> args = new ArrayList<String>();
        StringTokenizer tokenizer = new StringTokenizer(this.compilerArgumentsToUse);

        while (tokenizer.hasMoreTokens())
        {
            args.add(tokenizer.nextToken());
        }

        Iterator<CompilationJob> job_it = this.jobs.iterator();

        this.handler.startAction("Compilation", this.jobs.size());

        // check whether compiler is valid (but only if there are jobs)
        if (job_it.hasNext())
        {
            CompilationJob first_job = this.jobs.get(0);

            CompileResult check_result = first_job.checkCompiler(this.compilerToUse, args);
            if (!check_result.isContinue())
            {
                return check_result;
            }

        }

        int job_no = 0;

        while (job_it.hasNext())
        {
            CompilationJob job = job_it.next();

            this.handler.nextStep(job.getName(), job.getSize(), job_no++);

            CompileResult job_result = job.perform(this.compilerToUse, args);

            if (!job_result.isContinue())
            {
                return job_result;
            }
        }

        Debug.trace("compilation finished.");
        return new CompileResult();
    }

    private CompilationJob collectJobsRecursive(IXMLElement node, ArrayList classpath)
            throws Exception
    {
        Vector<IXMLElement> toplevel_tags = node.getChildren();
        ArrayList ourclasspath = (ArrayList) classpath.clone();
        ArrayList<File> files = new ArrayList<File>();

        for (int i = 0; i < toplevel_tags.size(); i++) {
            IXMLElement child = (IXMLElement) toplevel_tags.elementAt(i);

            if ("classpath".equals(child.getName()))
            {
                changeClassPath(ourclasspath, child);
            }
            else if ("job".equals(child.getName()))
            {
                CompilationJob subjob = collectJobsRecursive(child, ourclasspath);
                if (subjob != null)
                {
                    this.jobs.add(subjob);
                }
            }
            else if ("directory".equals(child.getName()))
            {
                String name = child.getAttribute("name");

                if (name != null)
                {
                    // substitute variables
                    String finalname = this.vs.substitute(name, "plain");

                    files.addAll(scanDirectory(new File(finalname)));
                }

            }
            else if ("file".equals(child.getName()))
            {
                String name = child.getAttribute("name");

                if (name != null)
                {
                    // substitute variables
                    String finalname = this.vs.substitute(name, "plain");

                    files.add(new File(finalname));
                }

            }
            else if ("packdepency".equals(child.getName()))
            {
                String name = child.getAttribute("name");

                if (name == null)
                {
                    System.out
                            .println("invalid compilation spec: <packdepency> without name attribute");
                    return null;
                }

                // check whether the wanted pack was selected for installation
                Iterator pack_it = this.idata.selectedPacks.iterator();
                boolean found = false;

                while (pack_it.hasNext())
                {
                    com.izforge.izpack.Pack pack = (com.izforge.izpack.Pack) pack_it.next();

                    if (pack.name.equals(name))
                    {
                        found = true;
                        break;
                    }
                }

                if (!found)
                {
                    Debug.trace("skipping job because pack " + name + " was not selected.");
                    return null;
                }

            }

        }

        if (files.size() > 0)
        {
            return new CompilationJob(this.handler, this.idata, node.getAttribute("name"), files, ourclasspath);
        }

        return null;
    }

    /**
     * helper: process a <code>&lt;classpath&gt;</code> tag.
     */
    private void changeClassPath(ArrayList classpath, IXMLElement child) throws Exception
    {
        String add = child.getAttribute("add");
        if (add != null)
        {
            add = this.vs.substitute(add, "plain");
            if (!new File(add).exists())
            {
                if (!this.handler.emitWarning("Invalid classpath", "The path " + add
                        + " could not be found.\nCompilation may fail."))
                {
                    throw new Exception("Classpath " + add + " does not exist.");
                }
            }
            else
            {
                classpath.add(this.vs.substitute(add, "plain"));
            }

        }

        String sub = child.getAttribute("sub");
        if (sub != null)
        {
            int cpidx = -1;
            sub = this.vs.substitute(sub, "plain");

            do
            {
                cpidx = classpath.indexOf(sub);
                classpath.remove(cpidx);
            }
            while (cpidx >= 0);

        }

    }

    /**
     * helper: recursively scan given directory.
     *
     * @return list of files found (might be empty)
     */
    private ArrayList<File> scanDirectory(File path)
    {
        Debug.trace("scanning directory " + path.getAbsolutePath());

        ArrayList<File> scan_result = new ArrayList<File>();

        if (!path.isDirectory())
        {
            return scan_result;
        }

        File[] entries = path.listFiles();

        for (File f : entries)
        {
            if (f == null)
            {
                continue;
            }

            if (f.isDirectory())
            {
                scan_result.addAll(scanDirectory(f));
            }
            else if ((f.isFile()) && (f.getName().toLowerCase().endsWith(".java")))
            {
                scan_result.add(f);
            }

        }

        return scan_result;
    }

    /**
     * a compilation job
     */
    private static class CompilationJob
    {

        private CompileHandler listener;

        private String name;

        private ArrayList<File> files;

        private ArrayList classpath;

        private LocaleDatabase langpack;

        private AutomatedInstallData idata;

        // XXX: figure that out (on runtime?)
        private static final int MAX_CMDLINE_SIZE = 4096;

        /**
         * Construct new compilation job.
         *
         * @param listener  The listener to report progress to.
         * @param idata     The installation data.
         * @param name      The name of the job.
         * @param files     The files to compile.
         * @param classpath The class path to use.
         */
        public CompilationJob(CompileHandler listener, AutomatedInstallData idata, String name,
                              ArrayList<File> files, ArrayList classpath)
        {
            this.listener = listener;
            this.idata = idata;
            this.langpack = idata.langpack;
            this.name = name;
            this.files = files;
            this.classpath = classpath;
        }

        /**
         * Get the name of the job.
         *
         * @return The name or an empty string if there is no name.
         */
        public String getName()
        {
            if (this.name != null)
            {
                return this.name;
            }

            return "";
        }

        /**
         * Get the number of files in this job.
         *
         * @return The number of files to compile.
         */
        public int getSize()
        {
            return this.files.size();
        }

        /**
         * Perform this job - start compilation.
         *
         * @param compiler  The compiler to use.
         * @param arguments The compiler arguments to use.
         * @return The result.
         */
        public CompileResult perform(String compiler, ArrayList<String> arguments)
        {
            Debug.trace("starting job " + this.name);
            // we have some maximum command line length - need to count
            int cmdline_len = 0;

            // used to collect the arguments for executing the compiler
            LinkedList<String> args = new LinkedList<String>(arguments);

            {
                Iterator<String> arg_it = args.iterator();
                while (arg_it.hasNext())
                {
                    cmdline_len += (arg_it.next()).length() + 1;
                }
            }

            boolean isEclipseCompiler = compiler.equalsIgnoreCase(ECLIPSE_COMPILER_NAME);

            // add compiler in front of arguments
            args.add(0, compiler);
            cmdline_len += compiler.length() + 1;

            // construct classpath argument for compiler
            // - collect all classpaths
            StringBuffer classpath_sb = new StringBuffer();
            Iterator cp_it = this.classpath.iterator();
            while (cp_it.hasNext())
            {
                String cp = (String) cp_it.next();
                if (classpath_sb.length() > 0)
                {
                    classpath_sb.append(File.pathSeparatorChar);
                }
                classpath_sb.append(new File(cp).getAbsolutePath());
            }

            String classpath_str = classpath_sb.toString();

            // - add classpath argument to command line
            if (classpath_str.length() > 0)
            {
                args.add("-classpath");
                cmdline_len += 11;
                args.add(classpath_str);
                cmdline_len += classpath_str.length() + 1;
            }

            // remember how many arguments we have which don't change for the
            // job
            int common_args_no = args.size();
            // remember how long the common command line is
            int common_args_len = cmdline_len;

            // used for execution
            FileExecutor executor = new FileExecutor();
            String output[] = new String[2];

            // used for displaying the progress bar
            String jobfiles = "";
            int fileno = 0;
            int last_fileno = 0;

            // now iterate over all files of this job
            Iterator<File> file_it = this.files.iterator();

            while (file_it.hasNext())
            {
                File f = file_it.next();

                String fpath = f.getAbsolutePath();

                Debug.trace("processing " + fpath);

                // we add the file _first_ to the arguments to have a better
                // chance to get something done if the command line is almost
                // MAX_CMDLINE_SIZE or even above
                fileno++;
                jobfiles += f.getName() + " ";
                args.add(fpath);
                cmdline_len += fpath.length();

                // start compilation if maximum command line length reached
                if (!isEclipseCompiler && cmdline_len >= MAX_CMDLINE_SIZE)
                {
                    Debug.trace("compiling " + jobfiles);

                    // display useful progress bar (avoid showing 100% while
                    // still compiling a lot)
                    this.listener.progress(last_fileno, jobfiles);
                    last_fileno = fileno;

                    int retval = runCompiler(executor, output, args);

                    // update progress bar: compilation of fileno files done
                    this.listener.progress(fileno, jobfiles);

                    if (retval != 0)
                    {
                        CompileResult result = new CompileResult(this.langpack
                                .getString("CompilePanel.error"), args, output[0],
                                output[1]);
                        this.listener.handleCompileError(result);
                        if (!result.isContinue())
                        {
                            return result;
                        }
                    }
                    else
                    {
                        // verify that all files have been compiled successfully
                        // I found that sometimes, no error code is returned
                        // although compilation failed.
                        Iterator<String> arg_it = args.listIterator(common_args_no);
                        while (arg_it.hasNext())
                        {
                            File java_file = new File(arg_it.next());

                            String basename = java_file.getName();
                            int dotpos = basename.lastIndexOf('.');
                            basename = basename.substring(0, dotpos) + ".class";
                            File class_file = new File(java_file.getParentFile(), basename);

                            if (!class_file.exists())
                            {
                                CompileResult result = new CompileResult(this.langpack
                                        .getString("CompilePanel.error.noclassfile")
                                        + java_file.getAbsolutePath(), args, output[0],
                                        output[1]);
                                this.listener.handleCompileError(result);
                                if (!result.isContinue())
                                {
                                    return result;
                                }
                                // don't continue any further
                                break;
                            }

                        }

                    }

                    // clean command line: remove files we just compiled
                    for (int i = args.size() - 1; i >= common_args_no; i--)
                    {
                        args.removeLast();
                    }

                    cmdline_len = common_args_len;
                    jobfiles = "";
                }

            }

            if (cmdline_len > common_args_len)
            {
                this.listener.progress(last_fileno, jobfiles);

                int retval = runCompiler(executor, output, args);

                if (!isEclipseCompiler)
                {
                    this.listener.progress(fileno, jobfiles);
                }

                if (retval != 0)
                {
                    CompileResult result = new CompileResult(this.langpack
                            .getString("CompilePanel.error"), args, output[0], output[1]);
                    this.listener.handleCompileError(result);
                    if (!result.isContinue())
                    {
                        return result;
                    }
                }
                else
                {
                    // verify that all files have been compiled successfully
                    // I found that sometimes, no error code is returned
                    // although compilation failed.
                    Iterator<String> arg_it = args.listIterator(common_args_no);
                    while (arg_it.hasNext())
                    {
                        File java_file = new File(arg_it.next());

                        String basename = java_file.getName();
                        int dotpos = basename.lastIndexOf('.');
                        basename = basename.substring(0, dotpos) + ".class";
                        File class_file = new File(java_file.getParentFile(), basename);

                        if (!class_file.exists())
                        {
                            CompileResult result = new CompileResult(this.langpack
                                    .getString("CompilePanel.error.noclassfile")
                                    + java_file.getAbsolutePath(), args, output[0],
                                    output[1]);
                            this.listener.handleCompileError(result);
                            if (!result.isContinue())
                            {
                                return result;
                            }
                            // don't continue any further
                            break;
                        }

                    }

                }

            }

            Debug.trace("job " + this.name + " done (" + fileno + " files compiled)");

            return new CompileResult();
        }

        /**
         * Internal helper method.
         *
         * @param executor The executor, only used when using external compiler.
         * @param output   The output from the compiler ([0] = stdout, [1] = stderr)
         * @return The result of the compilation.
         */
        private int runCompiler(FileExecutor executor, String[] output, List<String> cmdline)
        {
            if (cmdline.get(0).equals(ECLIPSE_COMPILER_NAME))
            {
                return runEclipseCompiler(output, cmdline);
            }

            return executor.executeCommand((String[]) cmdline.toArray(new String[cmdline.size()]), output);
        }

        private int runEclipseCompiler(String[] output, List<String> cmdline)
        {
            try
            {
                List<String> final_cmdline = new LinkedList<String>(cmdline);

                // remove compiler name from argument list
                final_cmdline.remove(0);

                Class eclipseCompiler = Class.forName(ECLIPSE_COMPILER_CLASS);

                Method compileMethod = eclipseCompiler.getMethod("main", new Class[]{String[].class});

                final_cmdline.add(0, "-noExit");
                final_cmdline.add(0, "-progress");
                final_cmdline.add(0, "-verbose");

                File _logfile = new File(this.idata.getInstallPath(), "compile-" + getName() + ".log");

                if (Debug.isTRACE())
                {
                    final_cmdline.add(0, _logfile.getPath());
                    final_cmdline.add(0, "-log");
                }

                // get log files / determine results...
                try
                {
                    // capture stdout and stderr
                    PrintStream _orgStdout = System.out;
                    PrintStream _orgStderr = System.err;
                    int error_count = 0;

                    try
                    {
                        ByteArrayOutputStream outStream = new ByteArrayOutputStream();
                        EclipseStdOutHandler ownStdout = new EclipseStdOutHandler(outStream, this.listener);
                        System.setOut(ownStdout);
                        ByteArrayOutputStream errStream = new ByteArrayOutputStream();
                        EclipseStdErrHandler ownStderr = new EclipseStdErrHandler(errStream, this.listener);
                        System.setErr(ownStderr);

                        compileMethod.invoke(null, new Object[]{final_cmdline.toArray(new String[final_cmdline.size()])});

                        // TODO: launch thread which updates the progress
                        output[0] = outStream.toString();
                        output[1] = errStream.toString();
                        error_count = ownStderr.getErrorCount();
                        // for debugging: write output to log files
                        if (error_count > 0 || Debug.isTRACE())
                        {
                            File _out = new File(_logfile.getPath() + ".stdout");
                            FileOutputStream _fout = new FileOutputStream(_out);
                            _fout.write(outStream.toByteArray());
                            _fout.close();
                            _out = new File(_logfile.getPath() + ".stderr");
                            _fout = new FileOutputStream(_out);
                            _fout.write(errStream.toByteArray());
                            _fout.close();
                        }

                    }
                    finally
                    {
                        System.setOut(_orgStdout);
                        System.setErr(_orgStderr);
                    }

                    if (error_count == 0)
                    {
                        return 0;
                    }

                    // TODO: construct human readable error message from log
                    this.listener.emitNotification("Compiler reported " + error_count + " errors");

                    return 1;
                }
                catch (FileNotFoundException fnfe)
                {
                    this.listener.emitError("error compiling", fnfe.getMessage());
                    return -1;
                }
                catch (IOException ioe)
                {
                    this.listener.emitError("error compiling", ioe.getMessage());
                    return -1;
                }

            }
            catch (ClassNotFoundException cnfe)
            {
                output[0] = "error getting eclipse compiler";
                output[1] = cnfe.getMessage();
                return -1;
            }
            catch (NoSuchMethodException nsme)
            {
                output[0] = "error getting eclipse compiler method";
                output[1] = nsme.getMessage();
                return -1;
            }
            catch (IllegalAccessException iae)
            {
                output[0] = "error calling eclipse compiler";
                output[1] = iae.getMessage();
                return -1;
            }
            catch (InvocationTargetException ite)
            {
                output[0] = "error calling eclipse compiler";
                output[1] = ite.getMessage();
                return -1;
            }

        }

        /**
         * Check whether the given compiler works.
         * <p/>
         * This performs two steps:
         * <ol>
         * <li>check whether we can successfully call "compiler -help"</li>
         * <li>check whether we can successfully call "compiler -help arguments" (not all compilers
         * return an error here)</li>
         * </ol>
         * <p/>
         * On failure, the method CompileHandler#errorCompile is called with a descriptive error
         * message.
         *
         * @param compiler  the compiler to use
         * @param arguments additional arguments to pass to the compiler
         * @return false on error
         */
        public CompileResult checkCompiler(String compiler, ArrayList<String> arguments)
        {
            // don't do further checks for eclipse compiler - it would exit
            if (compiler.equalsIgnoreCase(ECLIPSE_COMPILER_NAME))
            {
                return new CompileResult();
            }

            int retval = 0;
            FileExecutor executor = new FileExecutor();
            String[] output = new String[2];

            Debug.trace("checking whether \"" + compiler + " -help\" works");

            {
                List<String> args = new ArrayList<String>();
                args.add(compiler);
                args.add("-help");

                retval = runCompiler(executor, output, args);

                if (retval != 0)
                {
                    CompileResult result = new CompileResult(this.langpack
                            .getString("CompilePanel.error.compilernotfound"), args, output[0],
                            output[1]);
                    this.listener.handleCompileError(result);
                    if (!result.isContinue())
                    {
                        return result;
                    }
                }
            }

            Debug.trace("checking whether \"" + compiler + " -help +arguments\" works");

            // used to collect the arguments for executing the compiler
            LinkedList<String> args = new LinkedList<String>(arguments);

            // add -help argument to prevent the compiler from doing anything
            args.add(0, "-help");

            // add compiler in front of arguments
            args.add(0, compiler);

            // construct classpath argument for compiler
            // - collect all classpaths
            StringBuffer classpath_sb = new StringBuffer();
            Iterator cp_it = this.classpath.iterator();
            while (cp_it.hasNext())
            {
                String cp = (String) cp_it.next();
                if (classpath_sb.length() > 0)
                {
                    classpath_sb.append(File.pathSeparatorChar);
                }
                classpath_sb.append(new File(cp).getAbsolutePath());
            }

            String classpath_str = classpath_sb.toString();

            // - add classpath argument to command line
            if (classpath_str.length() > 0)
            {
                args.add("-classpath");
                args.add(classpath_str);
            }

            retval = runCompiler(executor, output, args);

            if (retval != 0)
            {
                CompileResult result = new CompileResult(this.langpack
                        .getString("CompilePanel.error.invalidarguments"), args, output[0],
                        output[1]);
                this.listener.handleCompileError(result);
                if (!result.isContinue())
                {
                    return result;
                }
            }

            return new CompileResult();
        }

    }

    /**
     * This PrintStream is used to track the Eclipse compiler output.
     * <p/>
     * It will pass on all println requests and report progress to the listener.
     */
    private static class EclipseStdOutHandler extends PrintStream
    {
        private CompileHandler listener;
        private StdOutParser parser;

        /**
         * Default constructor.
         *
         * @param anOutputStream The stream to wrap.
         * @param aHandler       the handler to use.
         */
        public EclipseStdOutHandler(final OutputStream anOutputStream, final CompileHandler aHandler)
        {
            // initialize with dummy stream (PrintStream needs it)
            super(anOutputStream);
            this.listener = aHandler;
            this.parser = new StdOutParser();
        }

        /**
         * Eclipse compiler hopefully only uses println(String).
         * <p/>
         * {@inheritDoc}
         */
        public void println(String x)
        {
            if (x.startsWith("[completed "))
            {
                int pos = x.lastIndexOf("#");
                int endpos = x.lastIndexOf("/");
                String fileno_str = x.substring(pos + 1, endpos - pos - 1);
                try
                {
                    int fileno = Integer.parseInt(fileno_str);
                    this.listener.progress(fileno, x);
                }
                catch (NumberFormatException _nfe)
                {
                    Debug.log("could not parse eclipse compiler output: '" + x + "': " + _nfe.getMessage());
                }
            }

            super.println(x);
        }

        /**
         * Unfortunately, the Eclipse compiler wraps System.out into a BufferedWriter.
         * <p/>
         * So we get whole buffers here and cannot do anything about it.
         * <p/>
         * {@inheritDoc}
         */
        public void write(byte[] buf, int off, int len)
        {
            super.write(buf, off, len);
            // we cannot convert back to string because the buffer might start
            // _inside_ a multibyte character
            // so we build a simple parser.
            int _fileno = this.parser.parse(buf, off, len);
            if (_fileno > -1)
            {
                this.listener.setSubStepNo(this.parser.getJobSize());
                this.listener.progress(_fileno, this.parser.getLastFilename());
            }
        }

    }

    /**
     * This PrintStream is used to track the Eclipse compiler error output.
     * <p/>
     * It will pass on all println requests and report progress to the listener.
     */
    private static class EclipseStdErrHandler extends PrintStream
    {
        // private CompileHandler listener;   // Unused
        private int errorCount = 0;
        private StdErrParser parser;

        /**
         * Default constructor.
         *
         * @param anOutputStream The stream to wrap.
         * @param aHandler       the handler to use.
         */
        public EclipseStdErrHandler(final OutputStream anOutputStream, final CompileHandler aHandler)
        {
            // initialize with dummy stream (PrintStream needs it)
            super(anOutputStream);
            // this.listener = aHandler; // TODO : reactivate this when we want to do something with it
            this.parser = new StdErrParser();
        }

        /**
         * Eclipse compiler hopefully only uses println(String).
         * <p/>
         * {@inheritDoc}
         */
        public void println(String x)
        {
            if (x.indexOf(". ERROR in ") > 0)
            {
                this.errorCount++;
            }

            super.println(x);
        }

        /**
         * Unfortunately, the Eclipse compiler wraps System.out into a BufferedWriter.
         * <p/>
         * So we get whole buffers here and cannot do anything about it.
         * <p/>
         * {@inheritDoc}
         */
        public void write(byte[] buf, int off, int len)
        {
            super.write(buf, off, len);
            // we cannot convert back to string because the buffer might start
            // _inside_ a multibyte character
            // so we build a simple parser.
            int _errno = this.parser.parse(buf, off, len);
            if (_errno > 0)
            {
                // TODO: emit error message instantly, but it may be incomplete yet
                // and we'd need to throw an exception to abort compilation
                this.errorCount += _errno;
            }
        }

        /**
         * Get the error state.
         *
         * @return true if there was an error detected.
         */
        public int getErrorCount()
        {
            return this.errorCount;
        }
    }

    /**
     * Common class for parsing Eclipse compiler output.
     */
    private static abstract class StreamParser
    {
        int idx;
        byte[] buffer;
        int offset;
        int length;
        byte[] lastIdentifier;
        int lastDigit;

        abstract int parse(byte[] buf, int off, int len);

        void init(byte[] buf, int off, int len)
        {
            this.buffer = buf;
            this.offset = off;
            this.length = len;
            this.idx = 0;
            this.lastIdentifier = null;
            this.lastDigit = -1;
        }

        int getNext()
        {
            if (this.offset + this.idx == this.length)
            {
                return Integer.MIN_VALUE;
            }

            return this.buffer[this.offset + this.idx++];
        }

        boolean findString(final String aString)
        {
            byte[] _search_bytes = aString.getBytes();
            int _search_idx = 0;

            do
            {
                int _c = getNext();
                if (_c == Integer.MIN_VALUE)
                {
                    return false;
                }

                if (_c == _search_bytes[_search_idx])
                {
                    _search_idx++;
                }
                else
                {
                    _search_idx = 0;
                    if (_c == _search_bytes[_search_idx])
                    {
                        _search_idx++;
                    }
                }
            }
            while (_search_idx < _search_bytes.length);

            return true;
        }

        boolean readIdentifier()
        {
            int _c;
            int _start_idx = this.idx;

            do
            {
                _c = getNext();
                // abort on incomplete string
                if (_c == Integer.MIN_VALUE)
                {
                    return false;
                }
            }
            while (!Character.isWhitespace((char) _c));

            this.idx--;
            this.lastIdentifier = new byte[this.idx - _start_idx];
            System.arraycopy(this.buffer, _start_idx, this.lastIdentifier, 0, this.idx - _start_idx);

            return true;
        }

        boolean readNumber()
        {
            int _c;
            int _start_idx = this.idx;

            do
            {
                _c = getNext();
                // abort on incomplete string
                if (_c == Integer.MIN_VALUE)
                {
                    return false;
                }
            }
            while (Character.isDigit((char) _c));

            this.idx--;
            String _digit_str = new String(this.buffer, _start_idx, this.idx - _start_idx);
            try
            {
                this.lastDigit = Integer.parseInt(_digit_str);
            }
            catch (NumberFormatException _nfe)
            {
                // should not happen - ignore                    
            }

            return true;
        }

        boolean skipSpaces()
        {
            int _c;

            do
            {
                _c = getNext();
                if (_c == Integer.MIN_VALUE)
                {
                    return false;
                }
            }
            while (Character.isWhitespace((char) _c));

            this.idx--;

            return true;
        }

    }

    private static class StdOutParser extends StreamParser
    {
        int fileno;
        int jobSize;
        String lastFilename;

        int parse(byte[] buf, int off, int len)
        {
            super.init(buf, off, len);
            this.fileno = -1;
            this.jobSize = -1;
            this.lastFilename = null;

            // a line looks like this:
            // [completed  /path/to/file.java - #1/2025]
            do
            {
                if (findString("[completed ")
                        && skipSpaces()
                        && readIdentifier())
                {
                    // remember file name
                    String filename = new String(this.lastIdentifier);

                    if (!skipSpaces())
                    {
                        continue;
                    }

                    int _c = getNext();
                    if (_c == Integer.MIN_VALUE)
                    {
                        return this.fileno;
                    }
                    if (_c != '-')
                    {
                        continue;
                    }

                    if (!skipSpaces())
                    {
                        continue;
                    }

                    _c = getNext();
                    if (_c == Integer.MIN_VALUE)
                    {
                        return this.fileno;
                    }
                    if (_c != '#')
                    {
                        continue;
                    }

                    if (!readNumber())
                    {
                        return this.fileno;
                    }

                    int _fileno = this.lastDigit;

                    _c = getNext();
                    if (_c == Integer.MIN_VALUE)
                    {
                        return this.fileno;
                    }
                    if (_c != '/')
                    {
                        continue;
                    }

                    if (!readNumber())
                    {
                        return this.fileno;
                    }

                    _c = getNext();
                    if (_c == Integer.MIN_VALUE)
                    {
                        return this.fileno;
                    }
                    if (_c != ']')
                    {
                        continue;
                    }

                    this.lastFilename = filename;
                    this.fileno = _fileno;
                    this.jobSize = this.lastDigit;
                    // continue parsing (figure out last occurence)
                }
                else
                {
                    return this.fileno;
                }

            }
            while (true);
        }

        String getLastFilename()
        {
            return this.lastFilename;
        }

        int getJobSize()
        {
            return this.jobSize;
        }
    }

    private static class StdErrParser extends StreamParser
    {
        int errorCount;

        int parse(byte[] buf, int off, int len)
        {
            super.init(buf, off, len);
            this.errorCount = 0;

            // a line looks like this:
            // [completed  /path/to/file.java - #1/2025]
            do
            {
                if (findString(". ERROR in "))
                {
                    this.errorCount++;
                }
                else
                {
                    return this.errorCount;
                }
            }
            while (true);
        }

        int getErrorCount()
        {
            return this.errorCount;
        }
    }
}
