/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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 org.apache.royale.compiler.clients;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.HashMap;
import java.util.Set;
import java.util.Comparator;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.MissingArgumentException;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.cli.PosixParser;
import org.apache.commons.cli.UnrecognizedOptionException;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.output.CountingOutputStream;

import org.apache.royale.abc.ABCConstants;
import org.apache.royale.abc.ABCLinker;
import org.apache.royale.compiler.clients.problems.CompilerProblemCategorizer;
import org.apache.royale.compiler.clients.problems.ProblemFormatter;
import org.apache.royale.compiler.clients.problems.ProblemPrinter;
import org.apache.royale.compiler.clients.problems.ProblemQuery;
import org.apache.royale.compiler.clients.problems.WorkspaceProblemFormatter;
import org.apache.royale.compiler.common.VersionInfo;
import org.apache.royale.compiler.config.CompilerDiagnosticsConstants;
import org.apache.royale.compiler.config.RSLSettings;
import org.apache.royale.compiler.filespecs.FileSpecification;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.clients.CLIFactory;
import org.apache.royale.compiler.internal.config.FrameInfo;
import org.apache.royale.compiler.internal.projects.ASCProject;
import org.apache.royale.compiler.internal.projects.CompilerProject;
import org.apache.royale.compiler.internal.projects.DefinitionPriority.BasePriority;
import org.apache.royale.compiler.internal.targets.AppSWFTarget;
import org.apache.royale.compiler.internal.tree.as.FileNode;
import org.apache.royale.compiler.internal.units.ABCCompilationUnit;
import org.apache.royale.compiler.internal.units.ASCompilationUnit;
import org.apache.royale.compiler.internal.units.ImportedASCompilationUnit;
import org.apache.royale.compiler.internal.units.SWCCompilationUnit;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.FileWriteProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.InvalidImportFileProblem;
import org.apache.royale.compiler.problems.MultipleExternallyVisibleDefinitionsProblem;
import org.apache.royale.compiler.problems.UnfoundPropertyProblem;
import org.apache.royale.compiler.targets.ISWFTarget;
import org.apache.royale.compiler.targets.ITargetSettings;
import org.apache.royale.compiler.targets.ITarget.TargetType;
import org.apache.royale.compiler.tree.as.IASNode;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.compiler.units.requests.IABCBytesRequestResult;
import org.apache.royale.swc.ISWC;
import org.apache.royale.swc.ISWCLibrary;
import org.apache.royale.swc.ISWCManager;
import org.apache.royale.swc.ISWCScript;
import org.apache.royale.swf.Header;
import org.apache.royale.swf.ISWF;
import org.apache.royale.swf.io.ISWFWriter;
import org.apache.royale.swf.io.SWFWriter;
import org.apache.royale.utils.FilenameNormalization;

import com.google.common.base.Joiner;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;

/**
 * ActionScript Compiler command-line interface.
 * <p>
 * Although {@code ASC} can parse most of the command-line options supported by
 * the old ASC, only a few of them are implemented at the moment.
 * 
 * @see <a href="https://zerowing.corp.adobe.com/display/compiler/ASC+Client">ASC client spec</a>
 */
public class ASC
{

    private static final String DOUBLE_QUOTE = "\"";
    
    /**
     * A target settings class to determine if a compilation unit is external.
     * 
     */
    class ASCTargetSettings implements ITargetSettings
    {
        
        ASCTargetSettings(String rootSourceFile)
        {
            this.rootSourceFile = rootSourceFile;
        }
        
        Set<String> externalLibrariesSet;
        final String rootSourceFile;
        
        @Override
        public boolean isAccessible()
        {
            return false;
        }

        @Override
        public boolean isDebugEnabled()
        {
            return getEmitDebugInfo();
        }

        @Override
        public boolean isTelemetryEnabled()
        {
            return false;
        }

        @Override
        public boolean isOptimized()
        {
            return getOptimize();
        }

        @Override
        public boolean useCompression()
        {
            return false;
        }

        @Override
        public boolean allowSubclassOverrides()
        {
            return false;
        }

        @Override
        public boolean areVerboseStacktracesEnabled()
        {
            return false;
        }

        @Override
        public Collection<String> getASMetadataNames()
        {
            return getEmitMetadata() ? null : Collections.<String>emptyList();
        }

        @Override
        public File getDefaultCSS()
        {
            return null;
        }

        @Override
        public int getDefaultBackgroundColor()
        {
            return 0;
        }

        @Override
        public int getDefaultFrameRate()
        {
            return getFrameRate();
        }

        @Override
        public boolean areDefaultScriptLimitsSet()
        {
            return false;
        }

        @Override
        public int getDefaultScriptTimeLimit()
        {
            return 0;
        }

        @Override
        public int getDefaultScriptRecursionLimit()
        {
            return 0;
        }

        @Override
        public int getDefaultWidth()
        {
            return getWidth();
        }

        @Override
        public int getDefaultHeight()
        {
            return getHeight();
        }

        @Override
        public Collection<String> getExterns()
        {
            return Collections.emptyList();
        }

        @Override
        public Collection<String> getIncludes()
        {
            return Collections.emptyList();
        }

        @Override
        public List<FrameInfo> getFrameLabels()
        {
            return Collections.emptyList();
        }

        @Override
        public String getSWFMetadata()
        {
            return null;
        }

        @Override
        public int getSWFVersion()
        {
            return DEFAULT_SWF_VERSION;
        }

        @Override
        public String getPreloaderClassName()
        {
            return null;
        }
        
        @Override
        public String getRootSourceFileName()
        {
            return rootSourceFile;
        }

        @Override
        public String getRootClassName()
        {
            return getSymbolClass();
        }

        @Override
        public boolean keepAllTypeSelectors()
        {
            return false;
        }

        @Override
        public boolean useNetwork()
        {
            return false;
        }

        @Override
        public List<File> getThemes()
        {
            return Collections.emptyList();
        }

        @Override
        public boolean removeUnusedRuntimeSharedLibraryPaths()
        {
            return false;
        }

        @Override
        public boolean verifyDigests()
        {
            return false;
        }

        @Override
        public Collection<File> getExternalLibraryPath()
        {
            if (externalLibraries == null || externalLibraries.size() == 0)
                return Collections.emptyList();
                
            List<File> result = new ArrayList<File>(externalLibraries.size());
            
            for (String path : externalLibraries)
            {
                result.add(new File(path));
            }
            
            return result;
        }

        @Override
        public Collection<File> getIncludeLibraries()
        {
            return Collections.emptyList();
        }

        @Override
        public Collection<File> getIncludeSources()
        {
            return Collections.emptyList();
        }

        @Override
        public List<String> getRuntimeSharedLibraries()
        {
            return Collections.emptyList();
        }

        @Override
        public List<RSLSettings> getRuntimeSharedLibraryPath()
        {
            return Collections.emptyList();
        }

        @Override
        public boolean useResourceBundleMetadata()
        {
            return false;
        }

        @Override
        public File getOutput()
        {
            return null;
        }

        @Override
        public Collection<String> getIncludeClasses()
        {
            return Collections.emptyList();
        }

        @Override
        public Map<String, File> getIncludeFiles()
        {
            return Collections.emptyMap();
        }

        @Override
        public Collection<String> getIncludeNamespaces()
        {
            return Collections.emptyList();
        }

        @Override
        public Collection<String> getIncludeResourceBundles()
        {
            return Collections.emptyList();
        }

        @Override
        public Map<String, File> getIncludeStyleSheets()
        {
            return Collections.emptyMap();
        }

        @Override
        public boolean isIncludeLookupOnlyEnabled()
        {
            return false;
        }

        @Override
        public boolean isLinkageExternal(String path)
        {
            if (externalLibrariesSet == null)
            {
                externalLibrariesSet = new HashSet<String>();
                
                if (externalLibraries != null)
                {
                    for (String externalPath : externalLibraries)
                    {
                        externalLibrariesSet.add(new File(externalPath).getAbsolutePath());
                    }                    
                }
            }
            
            return externalLibrariesSet.contains(path);
        }

        @Override
        public boolean useDirectBlit()
        {
            return false;
        }

        @Override
        public boolean useGPU()
        {
            return false;
        }

        @Override
        public List<String> getDefaultsCSSFiles()
        {
            return ImmutableList.of();
        }
        
        @Override
        public List<String> getExcludeDefaultsCSSFiles()
        {
            return ImmutableList.of();
        }
        
        @Override
        public File getLinkReport()
        {
            return null;
        }    
        
        @Override
        public File getSizeReport()
        {
            return null;
        }

        @Override
        public String getRoyaleMinimumSupportedVersion()
        {
            // Not used because ASC does not create SWCs.
            return null;
        }    

        @Override
        public boolean getMxmlChildrenAsData()
        {
            // Not used because ASC does handle MXML.
            return false;
        }    

        @Override
        public boolean getInfoFlex()
        {
            // Not used because ASC does handle MXML.
            return false;
        }    

        @Override
        public boolean getAllowSubclassOverrides()
        {
            // Not used because ASC is not used in cross-compiling.
            return false;
        }    

        @Override
        public String[] getMxmlImplicitImports()
        {
            // Not used because ASC does not create SWCs.
            return null;
        }    

        @Override
        public boolean getRemoveDeadCode()
        {
            return removeDeadCode;
        }
    }
    
    private static final int EXIT_CODE_SUCCESS = 0;
    private static final int EXIT_CODE_ERROR = 1;

    // for SWF generation
    private static final int DEFAULT_SWF_VERSION = 10;

    private static final int TARGET_AVM1 = 0; // Flash 9 VM
    private static final int TARGET_AVM2 = 1; // Flash 10 VM
    private static final int DEFAULT_TARGET_AVM = TARGET_AVM2; // Default to FP10
    private static final int DEFAULT_DIALECT = 9; // Earliest = 7, Latest = 11
    // CLIFactory is not thread-safe, get the options before instances are created.
    private static final Options ASC_OPTIONS = CLIFactory.getOptionsForASC();
    
    private PrintStream out;
    private PrintStream err;
    
    // Save problem info so that testsuite can access directly
    private ProblemQuery problemQuery;
    private ProblemFormatter problemFormatter;
       

    /**
     * Prints the asc executable command line header.
     */
    private void printHeader()
    {
        // This message should not be localized.
        out.println("ActionScript 3.0 Compiler for AVM+");
        out.println(VersionInfo.buildMessage());
    }

    private void printHelp(String commandLine, boolean showUsage)
    {
        PrintWriter pw = new PrintWriter(out);
        pw.println();
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp(pw, 72, commandLine, "       FILENAME...\noptions:", ASC_OPTIONS, 1, 3, null, showUsage);
        pw.flush();
    }

    private void printUsage(String commandLine)
    {
        PrintWriter pw = new PrintWriter(out);
        pw.println();
        HelpFormatter formatter = new HelpFormatter();
        formatter.printUsage(pw, 72, commandLine, ASC_OPTIONS);
        pw.println("       FILENAME...");
        pw.flush();
    }

    /**
     * Entry point for ASC command-line.
     */
    public static void main(String[] args)
    {
        final ASC asc = new ASC();
        final int exitStatus = asc.mainNoExit(args);
        System.exit(exitStatus);
    }
    
    /**
     * Do not call {@link System#exit(int)} here to allow unit testing.
     * 
     * Uses System.out and System.err as default streams
     * 
     * @param args arguments
     * @return exit code
     */
    public int mainNoExit(String[] args)
    {
        return mainNoExit(args, System.out, System.err);
    }
    
    /**
     * Do not call {@link System#exit(int)} here to allow unit testing.
     * 
     * @param args arguments
     * @param out stdout printstream
     * @param err stderr printstream
     * @return exit code
     */
    public int mainNoExit(String[] args, PrintStream out, PrintStream err)
    {
        // Set the Printstreams
        this.out = out;
        this.err = err;
        
        boolean showHelp = false;
        boolean clientSuccess = false;
        try
        {
            final CommandLineParser cliParser = new PosixParser();
            final CommandLine commandLine = cliParser.parse(ASC_OPTIONS, args);

            // Check if help was requested, otherwise use the command line to
            // instantiate an instance of the ASC client.
            if (commandLine.hasOption("h"))
                showHelp = true;
            else
                clientSuccess = createClient(commandLine);
        }
        catch (ParseException ex)
        {
            err.println("Command-Line Error: " + ex.getMessage());
        }

        int exitStatus = 0;
        if (clientSuccess && ! this.getSourceFilenames().isEmpty() )
        {
            exitStatus = this.main();
        }
        else
        {
            exitStatus = 1;
            if (showHelp)
            {
                printHeader();
                printHelp("asc", true);
            }
            else
            {
                printHeader();
                printUsage("asc");
            }
        }
        return exitStatus;
    }
    
    public String getProblems()
    {
        StringBuffer problems = new StringBuffer();
        if (problemQuery != null && problemQuery.hasFilteredProblems())
        {
            for (final ICompilerProblem problem : problemQuery.getFilteredProblems())
            {
                problems.append(problemFormatter.format(problem));
            }
        }
        return problems.toString().trim();
    }

    /**
     * This is the compilation process.
     * <ol>
     * <li>Create a workspace and AS project.</li>
     * <li>Configure the project according to the command-line options.</li>
     * <li>Compile and generate output files.</li>
     * <li>Print result and error messages if there are problems.</li>
     * </ol>
     * 
     * @return exit status code
     */
    private int main()
    {
        int exitStatus = EXIT_CODE_SUCCESS;

        final Workspace workspace = new Workspace();
        try
        {
            // source AS3 files
            final boolean success = compileSourceFiles(workspace, getSourceFilenames());
            if (!success)
                exitStatus = EXIT_CODE_ERROR;
        }
        catch (Exception e)
        {
            err.println(e.getMessage());
            e.printStackTrace();
            exitStatus = EXIT_CODE_ERROR;
        }
        finally
        {
            workspace.close();
        }

        return exitStatus;
    }

    /**
     * Create SWC compilation units from SWC files.
     * 
     * @param project project
     * @param swcFilePaths swc file paths. The iterator must maintain the order
     * of the swcs.
     * @return a collections of SWC compilation units
     */
    private Collection<ICompilationUnit> getCompilationUnitsForLibraries(
            final CompilerProject project,
            final Collection<String> swcFilePaths)
    {
        final List<ICompilationUnit> result = new ArrayList<ICompilationUnit>();
        final ISWCManager swcManager = project.getWorkspace().getSWCManager();
        int order = 0;
        
        for (final String swcFilePath : swcFilePaths)
        {
            if (!swcFilePath.toLowerCase().endsWith(".swc"))
                throw new RuntimeException("You can only import SWC libraries.");

            final ISWC swc = swcManager.get(new File(swcFilePath));

            for (final ISWCLibrary library : swc.getLibraries())
            {
                for (final ISWCScript script : library.getScripts())
                {
                    // Multiple definition in a script share the same compilation unit 
                    // with the same ABC byte code block.
                    final List<String> qnames = new ArrayList<String>(script.getDefinitions().size());
                    for (final String definitionQName : script.getDefinitions())
                    {
                        qnames.add(definitionQName.replace(":", "."));
                    }

                    final ICompilationUnit cu = new SWCCompilationUnit(
                            project, swc, library, script, qnames, order);
                    result.add(cu);
                }
            }
            
            order++;
        }

        return result;
    }
    
    /**
     * Minimally modifies the directory name read from the command line to make
     * a directory name we can use to write an output file.
     * 
     * @param inputDirectoryName directory name from the command line.
     * @return Directory name that is ends with {@link File#separator}.
     */
    private static String normalizeDirectoryName(String inputDirectoryName)
    {
        final String normalizedSeparators = FilenameUtils.separatorsToSystem(inputDirectoryName);
        if (inputDirectoryName.endsWith(File.separator))
            return normalizedSeparators;
        else
            return normalizedSeparators + File.separator;
    }
    
    /**
     * Compile one source file. Each source file has its own symbol table.
     * 
     * @param workspace workspace
     * @param sourceFilenames source filename
     * @throws InterruptedException compiler thread error
     * @return true compiled without problem
     */
    private boolean compileSourceFiles(final Workspace workspace, final List<String> sourceFilenames) throws InterruptedException
    {
        boolean success = true;
        long startTime = System.nanoTime();
        int problemCount = 0;
        
        //  Set up a problem query object to check the result of the compilation.
        //  Some problems found aren't ever relevant to ASC, and some depend on 
        //  the switches passed on the command line.
        problemQuery = new ProblemQuery();
        problemQuery.setShowProblemByClass(MultipleExternallyVisibleDefinitionsProblem.class, false);
        problemQuery.setShowProblemByClass(UnfoundPropertyProblem.class, false);
        problemQuery.setShowStrictSemantics(useStaticSemantics());    
        problemQuery.setShowWarnings(getShowWarnings());

        // process source AS3 files
        Set<ICompilationUnit> mainUnits = new LinkedHashSet<ICompilationUnit>(getSourceFilenames().size());
        final HashMap<ICompilationUnit, Integer> unitOrdering = new HashMap<ICompilationUnit, Integer>();

        ASCProject applicationProject = createProject(workspace, problemQuery);

        // Add any problems from parsing config vars supplied on the command line
        List<ICompilerProblem> configProblems = new ArrayList<ICompilerProblem>();
        applicationProject.collectProblems(configProblems);
        problemQuery.addAll(configProblems);

        int i = 0;
        for (final String sourceFilename : sourceFilenames)
        {
            // If we are not merging then create a new project
            // and set the compilation units.
            if (i > 0 && !getMergeABCs())
            {
                applicationProject = createProject(workspace, problemQuery);
                mainUnits.clear();
                unitOrdering.clear();
                problemQuery.clear();
            }

            final IFileSpecification sourceFileSpec = new FileSpecification(sourceFilename);
            workspace.fileAdded(sourceFileSpec);
            final ICompilationUnit cu = ASCompilationUnit.createMainCompilationUnitForASC(
                    applicationProject,
                    sourceFileSpec,
                    this);
            mainUnits.add(cu);
            unitOrdering.put(cu,unitOrdering.size());
            
            // add compilation unit to project
            applicationProject.addCompilationUnit(cu);
            applicationProject.updatePublicAndInternalDefinitions(Collections.singletonList(cu));
            
            // The logic that re-parses a garbage collected syntax tree, does not
            // know about the files included with the -in option, so we'll pin
            // the syntax tree here so we know we will never need to re-parse the
            // the synax tree for the root compilation unit.
            rootedSyntaxTrees.add(cu.getSyntaxTreeRequest().get().getAST());
            
            // syntax errors
            for (final ICompilationUnit compilationUnit : applicationProject.getCompilationUnits())
            {
                final ICompilerProblem[] problems = compilationUnit.getSyntaxTreeRequest().get().getProblems();
                problemQuery.addAll(problems);
            }
            
            //  Parse trees
            if ( getShowParseTrees() )
            {
                final String outputSyntaxFilename = FilenameUtils.removeExtension(sourceFilename).concat(".p");
                try
                {
                    PrintWriter syntaxFile = new PrintWriter(outputSyntaxFilename);
                    final IASNode ast = cu.getSyntaxTreeRequest().get().getAST();
                    if(ast instanceof FileNode)
                    {
                        // Parse the full tree and add the new problems found in the
                        // function bodies into the problem collection.
                        final FileNode fileNode = (FileNode)ast;
                        final ImmutableSet<ICompilerProblem> skeletonProblems = 
                                ImmutableSet.copyOf(fileNode.getProblems());
                    	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.FILE_NODE) == CompilerDiagnosticsConstants.FILE_NODE)
                    		System.out.println("ASC waiting for lock in populateFunctionNodes");
                        fileNode.populateFunctionNodes();
                    	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.FILE_NODE) == CompilerDiagnosticsConstants.FILE_NODE)
                    		System.out.println("ASC done with lock in populateFunctionNodes");
                        final ImmutableSet<ICompilerProblem> allProblems = 
                                ImmutableSet.copyOf(fileNode.getProblems());
                        
                        // Only add newly found problems. Otherwise, there will be
                        // duplicates in "problemQuery".
                        final SetView<ICompilerProblem> difference = Sets.difference(skeletonProblems, allProblems);
                        problemQuery.addAll(difference);
                    }
                    
                    syntaxFile.println(ast);
                    syntaxFile.flush();
                    syntaxFile.close();
                }
                catch (FileNotFoundException e)
                {
                    problemQuery.add(new FileWriteProblem(e));
                }
            }

            // output
            // For the merged case, wait until the last source file.
            // For the non-merged case, make each source file individually
            if (!getMergeABCs() || 
               (getMergeABCs() && (i == sourceFilenames.size() - 1)))
            {
                
                // Let's start up all the compilation units to try and get more threads generating code
                // at the same time.
                for (final ICompilationUnit compilationUnit : applicationProject.getCompilationUnits())
                {
                    compilationUnit.startBuildAsync(TargetType.SWF);
                }
                
                //  Run the resolveRefs() logic for as long as it's relevant.
                for (final ICompilationUnit compilationUnit : applicationProject.getCompilationUnits())
                {
                    final ICompilerProblem[] problems = compilationUnit.getOutgoingDependenciesRequest().get().getProblems();
                    problemQuery.addAll(problems);
                }

                String outputFileBaseName = FilenameUtils.getBaseName(sourceFilename);
                String outputDirectoryName = FilenameUtils.getFullPath(sourceFilename);
                
                // Apply user specified basename and output directory. The
                // basename is only changed ABCs are merged since each abc
                // needs a unique filename.
                if (getMergeABCs() && getOutputBasename() != null)
                    outputFileBaseName = getOutputBasename();
                
                final String specifiedOutputDirectory = getOutputDirectory();
                if (!Strings.isNullOrEmpty(specifiedOutputDirectory))
                    outputDirectoryName = normalizeDirectoryName(specifiedOutputDirectory);
                

                // Output to either a SWF or ABC file.
                if (isGenerateSWF())
                {
                    final boolean swfBuilt = generateSWF(outputDirectoryName, outputFileBaseName, applicationProject, 
                            mainUnits, sourceFilename, problemQuery, startTime);
                    if (!swfBuilt)
                        success = false;
                }
                else
                {
                    Collection<ICompilationUnit> units = mainUnits;
                    if(getMergeABCs())
                    {
                        // Run the topological sort to figure out which order to output the ABCs in
                        // Resorts to using commandline order rather than a filename based lexical sort in
                        // cases where there are no real dependencies between the scripts
                        units = applicationProject.getDependencyGraph().topologicalSort(mainUnits, 
                            new Comparator<ICompilationUnit>()
                            {
                                @Override
                                public int compare(ICompilationUnit o1, ICompilationUnit o2)
                                {
                                    return (unitOrdering.containsKey(o2) ? unitOrdering.get(o2) : 0) - (unitOrdering.containsKey(o1) ? unitOrdering.get(o1) : 0);
                                }
                            });
                        Collection<ICompilationUnit> sourceUnits = new ArrayList<ICompilationUnit>(mainUnits.size());
                        for(ICompilationUnit unit : units)
                        {
                            // The dependency graph will put all CompilationUnits in the results, but
                            // we only want the CUs for the source files, since the imports should not be merged
                            // into the resulting ABC
                            if(mainUnits.contains(unit))
                            {
                                sourceUnits.add(unit);
                            }
                        }
                        units = sourceUnits;
                    }
                    final boolean abcBuilt = generateABCFile(outputDirectoryName, outputFileBaseName, applicationProject, 
                            units, sourceFilename, problemQuery, startTime);
                    if (!abcBuilt)
                        success = false;
                }

                //*************************************
                // Report problems.
                //
                
                // let's make a categorizer, so we can differentiate errors and warnings
                CompilerProblemCategorizer compilerProblemCategorizer = new CompilerProblemCategorizer();
                problemFormatter = new WorkspaceProblemFormatter(workspace, compilerProblemCategorizer);
                ProblemPrinter printer = new ProblemPrinter(problemFormatter, err);
                problemCount += printer.printProblems(problemQuery.getFilteredProblems());
                
                startTime = System.nanoTime();
            }
            i++;
        }

        // If there were problems, print out the summary
        if (problemCount > 0)
        {
            Collection<ICompilerProblem> errors = new ArrayList<ICompilerProblem>();
            Collection<ICompilerProblem> warnings = new ArrayList<ICompilerProblem>();
            problemQuery.getErrorsAndWarnings(errors, warnings);
            
            int errorCount = errors.size();
            int warningCount = warnings.size();
          
            if (errorCount == 1)
            {
                err.println();
                err.println("1 error found");
            }
            else if (errorCount > 1)
            {
                err.println();
                err.println(errorCount + " errors found");
            }
           
            if (warningCount == 1)
            {
                err.println();
                err.println("1 warning found");
            }
            else if (warningCount > 1)
            {
                err.println();
                err.println(warningCount + " warnings found");
            }
    
            if (success && (errorCount > 0))
            { 
                success = false;      
            }
        }

        return success;
    }

    private ASCProject createProject(Workspace workspace, ProblemQuery problemQuery) throws InterruptedException
    {
        ASCProject applicationProject = new ASCProject(workspace, this.dialect == 10);
        setupConfigVars(applicationProject);
        applicationProject.setUseParallelCodeGeneration(getParallel());
        final List<ICompilationUnit> compilationUnits = new ArrayList<ICompilationUnit>();

        // import ABC files
        for (final String importFilename : getImportFilenames())
        {
            final ICompilationUnit cu;
            String lcFilename = importFilename.toLowerCase(); 
            if (lcFilename.endsWith(".abc"))
            {
                cu  = new ABCCompilationUnit(applicationProject, importFilename);
                compilationUnits.add(cu);
            }
            else if (lcFilename.endsWith(".as"))
            {
                cu = new ImportedASCompilationUnit(applicationProject,
                                                importFilename,
                                                BasePriority.SOURCE_LIST);
                compilationUnits.add(cu);
            }
            else
            {
                problemQuery.add(new InvalidImportFileProblem(importFilename));
            }
        }

        // get the compilation units for the external and internal libraries
        final List<String> libraries = new ArrayList<String>(externalLibraries.size() + internalLibraries.size());
        libraries.addAll(externalLibraries);
        libraries.addAll(internalLibraries);
        final Collection<ICompilationUnit> allLibraryCUs = getCompilationUnitsForLibraries(applicationProject, libraries);
        compilationUnits.addAll(allLibraryCUs);

        applicationProject.setCompilationUnits(compilationUnits);

        applicationProject.setEnableInlining(isInliningEnabled());

        return applicationProject;
    }

    /**
     * Transfer configuration variables supplied in -config to the project
     * settings.
     * 
     * @param applicationProject
     */
    private void setupConfigVars(ASCProject applicationProject)
    {
        Map<String, String> vars = getConfigVars();
        if (vars != null)
            applicationProject.addConfigVariables(vars);
    }

    /**
     * When {@code -swf} is not set, ASC compiles source file into an ABC file
     * by default.
     * 
     * @param applicationProject AS project
     * @param compilationUnits compilation units all of the source files
     * @param sourceFilename source file name
     * @param startTime time the build was started in nanoseconds
     * @return true if success
     * @throws InterruptedException error from compilation threads
     */
    private boolean generateABCFile(
            final String outputDirectoryName,
            final String outputBaseName,
            final ASCProject applicationProject,
            final Collection<ICompilationUnit> compilationUnits,
            final String sourceFilename,
            ProblemQuery problemQuery,
            long startTime)
            throws InterruptedException
    {
        boolean success = true;
        byte[] abcBytes = null;
        Collection<ICompilerProblem> fatalProblems = applicationProject.getFatalProblems();
        ArrayList<byte[]> abcList = new ArrayList<byte[]>();
        
        if( fatalProblems.isEmpty() )
        {
            for (ICompilationUnit cu : compilationUnits)
            {
                final IABCBytesRequestResult codegenResult = cu.getABCBytesRequest().get();
                abcBytes = codegenResult.getABCBytes();
                abcList.add(abcBytes);
                problemQuery.addAll(codegenResult.getProblems());
            }
        }
        else
        {
            problemQuery.addAll(fatalProblems);
        }

        try
        {

            //  Temporarily disable warnings so we can check for non-warnings.
            //  Note: To implement a "Warnings as errors" facility, change
            //  setShowWarnings()'s argument from literal false to query the
            //  relevant setting.
            problemQuery.setShowWarnings(false);

            if ( ! problemQuery.hasFilteredProblems() )
            {
                assert(abcBytes != null);

                try
                {
                    if( !getEmitDebugInfo() || !getABCFuture() || getMergeABCs() || getOptimize() || isInliningEnabled() )
                    {
                        ABCLinker.ABCLinkerSettings settings = new ABCLinker.ABCLinkerSettings();
                        settings.setStripDebugOpcodes(!getEmitDebugInfo());
                        settings.setOptimize(getOptimize());
                        settings.setEnableInlining(isInliningEnabled());
                        settings.setKeepMetadata(getEmitMetadata() ? null :
                            Collections.<String>emptyList());
                        settings.setStripGotoDefinitionHelp(true);
                        settings.setRemoveDeadCode(getRemoveDeadCode());

                        Collection<ICompilerProblem> linkerProblems = new ArrayList<ICompilerProblem>();
                        settings.setProblemsCollection(linkerProblems);
                        
                        abcBytes = ABCLinker.linkABC(abcList, getMajorABCVersion(), getMinorABCVersion(), settings);
                        problemQuery.addAll(linkerProblems);
                    }
                }
                catch(Exception e)
                {
                }
                
                final String outputABCBaseNameWithExt = outputBaseName + ".abc";
                final File outputABCFile = new File(outputDirectoryName + outputABCBaseNameWithExt);
                try
                {
                    final OutputStream abcFile = new BufferedOutputStream(new FileOutputStream(outputABCFile));
                    abcFile.write(abcBytes);
                    abcFile.flush();
                    abcFile.close();
                    out.format("%s, %d bytes written in %5.3f seconds\n",
                            outputABCFile.toString(), 
                            abcBytes.length,
                            (System.nanoTime() - startTime) / 1e9 );
                }
                catch (IOException e)
                {
                    problemQuery.add(new FileWriteProblem(e));
                    success = false;
                }
            }
            else
            {
                success = false;
            }
        }
        finally
        {
            problemQuery.setShowWarnings(getShowWarnings());
        }

        return success;
    }

    private int getMinorABCVersion()
    {
        return ABCConstants.VERSION_ABC_MINOR_FP10;
    }

    private int getMajorABCVersion()
    {
        return ABCConstants.VERSION_ABC_MAJOR_FP10;
    }

    /**
     * When {@code -swf} option is set, ASC compiles the source files into a SWF
     * file.
     * 
     * @param applicationProject application project
     * @param compilationUnits compilation unit(s) for the source file(s)
     * @param sourceFilename source file name
     * @param startTime time the build was started in nanoseconds
     * @return true if success
     * @throws InterruptedException error from compilation threads
     */
    private boolean generateSWF(
            String outputDirectoryName,
            String outputBaseName,
            final ASCProject applicationProject,
            final Set<ICompilationUnit> compilationUnits,
            final String sourceFilename,
            ProblemQuery problemQuery,
            long startTime)
            throws InterruptedException
    {
        boolean success = true;
        final ArrayList<ICompilerProblem> problemsBuildingSWF = new ArrayList<ICompilerProblem>();
        final ISWFTarget target = new AppSWFTarget(applicationProject, new ASCTargetSettings(sourceFilename), null, 
                compilationUnits);
        final ISWF swf = target.build(problemsBuildingSWF);

        if (swf != null)
        {
            swf.setTopLevelClass(getSymbolClass());

            final ISWFWriter writer = new SWFWriter(swf, Header.Compression.NONE);
            final String outputFileNameWithExt = outputBaseName + ".swf";
            final File outputFile = new File(outputDirectoryName + outputFileNameWithExt);
            try
            {
                CountingOutputStream output = new CountingOutputStream(
                        new BufferedOutputStream(new FileOutputStream(outputFile)));

                writer.writeTo(output);
                output.flush();
                output.close();
                writer.close();

                out.format("%s, %d bytes written in %5.3f seconds\n",
                        outputFile.toString(), 
                        output.getByteCount(),
                        (System.nanoTime() - startTime) / 1e9 );
            }
            catch (IOException e)
            {
                problemQuery.add(new FileWriteProblem(e));
                success = false;
            }
        }
        else
        {
            err.println("Unable to build SWF.");
            success = false;
        }

        problemQuery.addAll(problemsBuildingSWF);

        return success;
    }
    
    /**
     * Map from historical ASC language identifiers
     * to {@link Locale} objects.
     */
    private static final Map<String, Locale> localeMap =
        new ImmutableMap.Builder<String, Locale>()
        .put("EN", Locale.ENGLISH)
        .put("CN", Locale.SIMPLIFIED_CHINESE)
        .put("CS", new Locale("cs"))
        .put("DK", new Locale("da"))
        .put("DE", Locale.GERMAN)
        .put("ES", new Locale("es"))
        .put("FI", new Locale("fi"))
        .put("FR", Locale.FRENCH)
        .put("IT", Locale.ITALIAN)
        .put("JP", Locale.JAPANESE)
        .put("KR", Locale.KOREAN)
        .put("NO", new Locale("nb"))
        .put("NL", new Locale("nl"))
        .put("PL", new Locale("pl"))
        .put("BR", new Locale("pt"))
        .put("RU", new Locale("ru"))
        .put("SE", new Locale("sv"))
        .put("TR", new Locale("tr"))
        .put("TW", Locale.TRADITIONAL_CHINESE)
        .build();
    
    /**
     * Helper method used by command line parsing logic to convert historical
     * ASC language codes into a java {@link Locale} objecst.
     * 
     * @param language ASC language code to convert
     * @return A {@link Locale} object for the specified asc language code.
     * @throws MissingArgumentException Thrown if the specified language code is
     * not a valid asc language code.
     */
    private static Locale getLocaleForLanguage(String language) throws MissingArgumentException
    {
        Locale result = localeMap.get(language);
        if (result != null)
            return result;
        throw new MissingArgumentException("Language option must be one of: " + Joiner.on(", ").join(localeMap.keySet()));
    }
    

    /**
     * Apache Common CLI did the lexer work. This function does the parser work
     * to construct an {@code ASC} job from the command-line options.
     * 
     * @param line - the tokenized command-line
     * @return a new ASC client for the given command-line configuration; null
     * if no arguments were given.
     */
    private Boolean createClient(final CommandLine line) throws ParseException
    {
        // First, process parsed command line options.
        final Option[] options = line.getOptions();

        if (options == null)
            return false;

        for (int i = 0; i < options.length; i++)
        {
            final Option option = options[i];
            final String shortName = option.getOpt();

            if ("import".equals(shortName))
            {
                String[] imports = option.getValues();
                for (int j = 0; j < imports.length; j++)
                {
                    this.addImportFilename(imports[j]);
                }
            }
            else if ("in".equals(shortName))
            {
                String[] includes = option.getValues();
                for (int j = 0; j < includes.length; j++)
                {
                    this.addIncludeFilename(includes[j]);
                }
            }
            else if ("swf".equals(shortName))
            {
                String[] swfValues = option.getValue().split(",");
                if (swfValues.length < 3)
                    throw new MissingArgumentException("The swf option requires three arguments, only " + swfValues.length + " were found.");
                
                for (int j = 0; j < swfValues.length; j++)
                {
                    String value = swfValues[j];
                    if (j == 0)
                        this.setSymbolClass(value);
                    else if (j == 1)
                        this.setWidth(value);
                    else if (j == 2)
                        this.setHeight(value);
                    else if (j == 3)
                        this.setFrameRate(value);
                }
            }
            else if ("use".equals(shortName))
            {
                String[] namespaces = option.getValues();
                for (String namespace : namespaces)
                {
                    this.addNamespace(namespace);
                }
            }
            else if ("config".equals(shortName))
            {
                String[] config = option.getValues();
                if( config.length == 2 )
                {
                    // The config option will have been split around '='
                    // e.g. CONFIG::Foo='hi' will be split into
                    // 2 values - 'CONFIG::Foo' and 'hi'
                    String name = config[0];
                    String value = config[1];
                    value = fixupMissingQuote(value);
                    this.putConfigVar(name, value);
                }
            }
            else if ("strict".equals(shortName) || "!".equals(shortName))
            {
                this.setUseStaticSemantics(true);
            }
            else if ("d".equals(shortName))
            {
                this.setEmitDebugInfo(true);
            }
            else if ("warnings".equals(shortName) || "coach".equals(shortName))
            {
                if ("coach".equals(shortName))
                    err.println("'coach' has been deprecated. Please use 'warnings' instead.");
                this.setShowWarnings(true);
            }
            else if ("log".equals(shortName))
            {
                this.setShowLog(true);
            }
            else if ("md".equals(shortName))
            {
                this.setEmitMetadata(true);
            }
            else if ("merge".equals(shortName))
            {
                this.setMergeABCs(true);
            }
            else if ("language".equals(shortName))
            {
                String value = option.getValue();
                this.setLocale(getLocaleForLanguage(value));
            }
            else if ("doc".equals(shortName))
            {
                this.setEmitDocInfo(true);
            }
            else if ("avmtarget".equals(shortName))
            {
                String value = option.getValue();
                this.setTargetAVM(value);
            }
            else if ("AS3".equals(shortName))
            {
                this.setDialect("AS3");
            }
            else if ("ES".equals(shortName))
            {
                this.setDialect("ES");
            }
            else if ("o".equalsIgnoreCase(shortName) || "optimize".equalsIgnoreCase(shortName))
            {
                this.setOptimize(true);
            }
            else if ("o2".equalsIgnoreCase(shortName))
            {
                this.setOptimize(true);
            }
            else if ("out".equalsIgnoreCase(shortName))
            {
                this.setOutputBasename(option.getValue());
            }
            else if ("outdir".equalsIgnoreCase(shortName))
            {
                this.setOutputDirectory(option.getValue());
            }
            else if ("abcfuture".equals(shortName))
            {
                this.setABCFuture(true);
            }
            else if ("p".equals(shortName))
            {
                this.setShowParseTrees(true);
            }
            else if ("i".equals(shortName))
            {
                this.setShowInstructions(true);
            }
            else if ("m".equals(shortName))
            {
                this.setShowMachineCode(true);
            }
            else if ("f".equals(shortName))
            {
                this.setShowFlowGraph(true);
            }
            else if ("exe".equals(shortName))
            {
                String exe = option.getValue();
                this.setAvmplusFilename(exe);
            }
            else if ("movieclip".equals(shortName))
            {
                this.setMakeMovieClip(true);
            }
            else if ("ES4".equals(shortName))
            {
                this.setDialect("ES4");
            }
            else if ("li".equals(shortName))
            {
                this.internalLibraries.add(option.getValue());
            }
            else if ("le".equals(shortName))
            {
                this.externalLibraries.add(option.getValue());
            }
            else if ("parallel".equals(shortName))
            {
                this.setParallel(true);
            }
            else if ("inline".equals(shortName))
            {
                this.setMergeABCs(true); // inlining requires merging of ABCs
                this.setEnableInlining(true);
            }
            else if ( "removedeadcode".equals(shortName) )
            {
                this.setRemoveDeadCode(true);
            }
            else
            {
                throw new UnrecognizedOptionException("Unrecognized option '" + shortName + "'", shortName);
            }
        }

        // Then any remaining arguments that were not options are interpreted as
        // source files to compile.
        final String[] remainingArgs = line.getArgs();
        if (remainingArgs != null)
        {
            for (int i = 0; i < remainingArgs.length; i++)
            {
                this.addSourceFilename(remainingArgs[i]);
            }
        }
        else
        {
            throw new MissingArgumentException("At least one source file must be specified after the list of options.");
        }

        return true;
    }

    /**
     * The command line parser strips leading and trailing quotes from
     * arguments. For example, -config CONFIG::foo="test" gets parsed into 
     * -config CONFIG::foo="test. So here we will add the missing quote
     * back.
     * 
     * @param value the config value to fix up.
     * @return the fixed up config value.
     */
    private String fixupMissingQuote(String value)
    {
        // Look for unbalanced set of double quotes. If we have an odd number
        // of double quotes then append a double quote to the end of string,
        // assuming that this quote was removed by the parser.
        // If the value ends with a quote, then for sure a quote must have
        // been removed (could have been "");
        if (value.endsWith(DOUBLE_QUOTE))
        {
            value = value.concat(DOUBLE_QUOTE);            
        }
        else
        {
            String[] values = value.split(DOUBLE_QUOTE);
            
            // An even number of values means an odd
            // number of quotes.
            if (values.length % 2 == 0)
                value = value.concat(DOUBLE_QUOTE);            
        }
        
        return value;
    }

    // Inputs
    private List<String> sourceFilenames;
    private List<String> includeFilenames;
    private List<String> importFilenames;
    private Map<String, String> configVars;
    private List<String> useNamespaces;
    private final List<String> internalLibraries = new ArrayList<String>(0);
    private final List<String> externalLibraries = new ArrayList<String>(0);

    private int apiVersion = -1;
    private int dialect = DEFAULT_DIALECT;
    private int targetAVM = DEFAULT_TARGET_AVM;
    private boolean useStaticSemantics = false;
    private boolean showWarnings = false;
    private boolean useSanityMode = false; // TODO: Is this still relevant?
    private Locale locale = Locale.ENGLISH;

    // SWF
    private String symbolClass = null;
    private int width = 550;
    private int height = 400;
    private int frameRate = 12;
    private boolean emitDebugInfo = false;
    private boolean emitDocInfo = false;
    private boolean emitMetadata = false;

    private boolean abcFuture = false;

    private boolean makeMovieClip = false; // TODO: What did this do?
    private String avmplusFilename = null; // TODO: Do we still need to make a projector?

    // Compiler Debugging - TODO: Do we need to still support these? Can we use a different format?
    private boolean showBytes = false;
    private boolean showParseTrees = false;
    private boolean showInstructions = false;
    private boolean showLineNumbers = false;
    private boolean showMachineCode = false;
    private boolean showFlowGraph = false;
    private boolean showLog = false;
    
    // Post Processing
    private boolean optimize = false;
    private List<String> optimizerConfigs = null;
    private boolean mergeABCs = false;
    private String outputDirectory = null;
    private String outputBasename = null;
    
    // Use parallel code generation of method bodies?
    private boolean parallel = false;

    // Use function inlining
    private boolean enableInlining = false;

    // Remove dead code
    private boolean removeDeadCode = false;

    // List of IASNodes that are the root syntax tree nodes
    // of all the main compilation units compiled by an asc instance.
    // This is needed to prevent the GC from collecting our syntax tree.
    // We can't allow the GC to collect the syntax trees because the code that
    // re-parses a syntax tree if it is needed again does not know about
    // the -in options.
    private final ArrayList<IASNode> rootedSyntaxTrees = new ArrayList<IASNode>();

    /**
     * @return the list of source files to compile
     */
    public List<String> getSourceFilenames()
    {
        if (sourceFilenames == null)
            sourceFilenames = new ArrayList<String>();

        return sourceFilenames;
    }

    /**
     * @param source - a root source filename to compile
     */
    public void addSourceFilename(String source)
    {
        getSourceFilenames().add(source);
    }

    /**
     * @return the list of source filenames to include in compilation
     */
    public List<String> getIncludeFilenames()
    {
        if (includeFilenames == null)
            includeFilenames = new ArrayList<String>();

        return includeFilenames;
    }

    /**
     * @param filename - a source filename to include in compilation
     */
    public void addIncludeFilename(String filename)
    {
        final String includeFile = FilenameNormalization.normalize(filename);
        getIncludeFilenames().add(includeFile);
    }

    /**
     * @return the list of ABC filenames to import
     */
    public List<String> getImportFilenames()
    {
        if (importFilenames == null)
            importFilenames = new ArrayList<String>();

        return importFilenames;
    }

    /**
     * @param filename - an ABC filename to import
     */
    public void addImportFilename(String filename)
    {
        getImportFilenames().add(filename);
    }

    public List<String> getInternalLibraries()
    {
        return internalLibraries;
    }

    public List<String> getExternalLibraries()
    {
        return externalLibraries;
    }

    /**
     * @return the map of config namespace variables that will be used to
     * support conditional compilation
     */
    public Map<String, String> getConfigVars()
    {
        if (configVars == null)
            configVars = new LinkedHashMap<String, String>();

        return configVars;
    }

    /**
     * Sets a config var key/value pair used in conditional compilation.
     * 
     * @param key - the config var name
     * @param value - the config var value
     */
    public void putConfigVar(String key, String value)
    {
        getConfigVars().put(key, value);
    }

    /**
     * @return the list of namespaces to automatically use (open) during
     * compilation
     */
    public List<String> getNamespaces()
    {
        if (useNamespaces == null)
            useNamespaces = new ArrayList<String>();

        return useNamespaces;
    }

    /**
     * @param namespace - a namespace to be automatically used (opened) during
     * compilation
     */
    public void addNamespace(String namespace)
    {
        getNamespaces().add(namespace);
    }

    /**
     * @return the native API version of the runtime
     */
    public int getApiVersion()
    {
        return apiVersion;
    }

    /**
     * @param version - the native API version of the runtime
     */
    public void setApiVersion(String version)
    {
        this.apiVersion = Integer.parseInt(version.trim());
    }

    /**
     * @return the ActionScript language dialect to use during compilation
     */
    public int getDialect()
    {
        return dialect;
    }

    /**
     * @param dialect - the ActionScript language dialect to set for compilation
     */
    public void setDialect(String dialect)
    {
        if ("AS3".equalsIgnoreCase(dialect))
            this.dialect = 10;
        else if ("ES".equalsIgnoreCase(dialect))
            this.dialect = 9;
        else if ("ES4".equalsIgnoreCase(dialect))
            this.dialect = 11;
        else
            throw new IllegalArgumentException("Illegal dialect '" + dialect + "'");
    }

    /**
     * @return the version of the ActionScript Virtual Machine that generated
     * byte code should target, either 1 for AVM1 or 2 for AVM2.
     */
    public int getTargetAVM()
    {
        return targetAVM;
    }

    /**
     * @param version - the version of the ActionScript Virtual Machine that
     * generated byte code should target, either 1 for AVM1 or 2 for AVM2.
     */
    public void setTargetAVM(String version)
    {
        int v = Integer.parseInt(version.trim());
        if (v == TARGET_AVM1 || v == TARGET_AVM2)
            this.targetAVM = v;
        else
            throw new IllegalArgumentException("Illegal target AVM version '" + version + "'");
    }

    /**
     * @return whether static semantics should be used?
     */
    public boolean useStaticSemantics()
    {
        return useStaticSemantics;
    }

    /**
     * @param useStaticSemantics - determines whether static semantics should be
     * used? 
     */
    public void setUseStaticSemantics(boolean useStaticSemantics)
    {
        this.useStaticSemantics = useStaticSemantics;
    }
    /**
     * @return whether warnings for common ActionScript mistakes should be shown
     */
    public boolean getShowWarnings()
    {
        return showWarnings;
    }

    /**
     * @param showWarnings - determines whether warnings for common ActionScript
     * mistakes should be shown
     */
    public void setShowWarnings(boolean showWarnings)
    {
        this.showWarnings = showWarnings;
    }

    /**
     * @return whether system-independent error/warning output should be used
     * (appropriate for sanity testing)
     */
    public boolean getUseSanityMode()
    {
        return useSanityMode;
    }

    /**
     * @param useSanityMode - determines whether system-independent
     * error/warning output should be used
     */
    public void setUseSanityMode(boolean useSanityMode)
    {
        this.useSanityMode = useSanityMode;
    }

    /**
     * @return the locale to use for compiler errors and warnings
     */
    public Locale getLocale()
    {
        return locale;
    }

    /**
     * @param locale - the locale to use for compiler errors and warnings
     */
    public void setLocale(Locale locale)
    {
        this.locale = locale;
    }

    /**
     * @return whether a movieclip should be made? TODO: What did this option
     * do?
     */
    public boolean getMakeMovieClip()
    {
        return makeMovieClip;
    }

    /**
     * @param makeMovieClip - determines whether a movieclip should be made?
     */
    public void setMakeMovieClip(boolean makeMovieClip)
    {
        this.makeMovieClip = makeMovieClip;
    }

    /**
     * @return the name of the top-level SymbolClass to set in a SWF
     */
    public String getSymbolClass()
    {
        return symbolClass;
    }

    /**
     * @param symbolClass - the name of top-level SymbolClass to use in a SWF
     */
    public void setSymbolClass(String symbolClass)
    {
        this.symbolClass = symbolClass;
    }

    /**
     * @return true if {@code -swf} argument is set.
     */
    public boolean isGenerateSWF()
    {
        return this.symbolClass != null;
    }

    /**
     * @return the SWF stage width
     */
    public int getWidth()
    {
        return width;
    }

    /**
     * @param width - the SWF stage width
     */
    public void setWidth(String width)
    {
        this.width = Integer.parseInt(width.trim());
    }

    /**
     * @return the SWF stage height
     */
    public int getHeight()
    {
        return height;
    }

    /**
     * @param height - the SWF stage height
     */
    public void setHeight(String height)
    {
        this.height = Integer.parseInt(height.trim());
    }

    /**
     * @return the SWF frame rate
     */
    public int getFrameRate()
    {
        return frameRate;
    }

    /**
     * @param frameRate - the SWF frame rate
     */
    public void setFrameRate(String frameRate)
    {
        this.frameRate = Integer.parseInt(frameRate.trim());
    }

    /**
     * @return whether debug info should be emitted into the byte code
     */
    public boolean getEmitDebugInfo()
    {
        return emitDebugInfo;
    }

    /**
     * @return whether we should target newer abc version
     */
    public boolean getABCFuture()
    {
        return abcFuture;
    }

    /**
     * @param b determines if we should emit a 47.14 ABC or not
     */
    public void setABCFuture(boolean b)
    {
        abcFuture = b;
    }
    /**
     * @param emitDebugInfo - determines whether debug info should be emitted
     * into the byte code
     */
    public void setEmitDebugInfo(boolean emitDebugInfo)
    {
        this.emitDebugInfo = emitDebugInfo;
    }

    /**
     * @return whether asdoc information should be emitted into the byte code
     */
    public boolean getEmitDocInfo()
    {
        return emitDocInfo;
    }

    /**
     * @param emitDocInfo - determines whether asdoc info should be emitted into
     * the byte code
     */
    public void setEmitDocInfo(boolean emitDocInfo)
    {
        this.emitDocInfo = emitDocInfo;
    }

    /**
     * @return whether metadata should be emitted into the byte code
     */
    public boolean getEmitMetadata()
    {
        return emitMetadata;
    }

    /**
     * @param emitMetadata - determines whether metadata should be emitted into
     * the byte code
     */
    public void setEmitMetadata(boolean emitMetadata)
    {
        this.emitMetadata = emitMetadata;
    }

    /**
     * @return the filename of the avmplus executable to use to make a projector
     * file
     */
    public String getAvmplusFilename()
    {
        return avmplusFilename;
    }

    /**
     * @param filename - the avmplus executable filename to use to create a
     * projector file
     */
    public void setAvmplusFilename(String filename)
    {
        this.avmplusFilename = filename;
    }

    /**
     * @return whether the byte codes should be shown in a dump of the generated
     * ABC
     */
    public boolean getShowBytes()
    {
        return showBytes;
    }

    /**
     * @param showBytes - determines whether bytes should be shown in a dump of
     * the generated ABC
     */
    public void setShowBytes(boolean showBytes)
    {
        this.showBytes = showBytes;
    }

    /**
     * @return whether the parse tree should be written to a .p file.
     */
    public boolean getShowParseTrees()
    {
        return showParseTrees;
    }

    /**
     * @param showParseTrees - determines whether the parse tree should be
     * written to a .p file
     */
    public void setShowParseTrees(boolean showParseTrees)
    {
        this.showParseTrees = showParseTrees;
    }

    /**
     * @return whether intermediate code should be written to a .il file
     */
    public boolean getShowInstructions()
    {
        return showInstructions;
    }

    /**
     * @param showInstructions - determines whether intermediate code should be
     * written to a .il file
     */
    public void setShowInstructions(boolean showInstructions)
    {
        this.showInstructions = showInstructions;
    }

    /**
     * @return whether line numbers should be shown in the dump of generated ABC
     * bytes
     */
    public boolean getShowLineNumbers()
    {
        return showLineNumbers;
    }

    /**
     * @param showLineNumbers - determines whether line numbers should be shown
     * in the dump of generated ABC bytes
     */
    public void setShowLineNumbers(boolean showLineNumbers)
    {
        this.showLineNumbers = showLineNumbers;
    }

    /**
     * @return whether AVM+ assembly code should be included in the .il file
     */
    public boolean getShowMachineCode()
    {
        return showMachineCode;
    }

    /**
     * @param showMachineCode - determines whether AVM+ assembly code should be
     * included in the .il file
     */
    public void setShowMachineCode(boolean showMachineCode)
    {
        this.showMachineCode = showMachineCode;
    }

    /**
     * @return whether the flow graph should be printed to standard out
     */
    public boolean getShowFlowGraph()
    {
        return showFlowGraph;
    }

    /**
     * @param showFlowGraph - determines whether the flow graph should be
     * printed to standard out
     */
    public void setShowFlowGraph(boolean showFlowGraph)
    {
        this.showFlowGraph = showFlowGraph;
    }

    /**
     * @return whether logging information should be shown
     */
    public boolean getShowLog()
    {
        return showLog;
    }

    /**
     * @param showLog - determines whether logging information should be shown
     */
    public void setShowLog(boolean showLog)
    {
        this.showLog = showLog;
    }

    /**
     * @return whether the ABC file should be optimized
     */
    public boolean getOptimize()
    {
        return optimize;
    }

    /**
     * @param optimize - determines whether the generated ABC should be
     * optimized
     */
    public void setOptimize(boolean optimize)
    {
        this.optimize = optimize;
    }

    /**
     * @return the configuration options for the optimizer
     */
    public List<String> getOptimizerConfigs()
    {
        if (optimizerConfigs == null)
            optimizerConfigs = new ArrayList<String>();

        return optimizerConfigs;
    }

    /**
     * @param value - a configuration option for the optimizer, potentially
     * specified as a 'name=value' String
     */
    public void addOptimizerConfig(String value)
    {
        getOptimizerConfigs().add(value);
    }
    
    /**
     * @return true if the ABC bytes should be merged into a single output 
     * file, false otherwise.
     */
    public boolean getMergeABCs()
    {
        return mergeABCs;
    }

    /**
     * @param mergeABCs true to turn on ABC merging, false to turn off.
     */
    public void setMergeABCs(boolean mergeABCs)
    {
        this.mergeABCs = mergeABCs;
    }

    /**
     * Get the user specified output directory.
     * Set using the "-outdir" option.
     * 
     * @return user specified directory. May be null if no directory was 
     * specified.
     */
    public String getOutputDirectory()
    {
        return outputDirectory;
    }

    /**
     * Set the output directory from the "-outdir" option.
     * 
     * @param outputDirectory A path string.
     */
    public void setOutputDirectory(String outputDirectory)
    {
        this.outputDirectory = outputDirectory;
    }
    
    /**
     * Get the user specified basename. This option only makes sense when
     * merging ABCs. The basename can be specified using the "-out" option.
     * 
     * @return the basename of output files. May be null if no basename was
     * specified.
     */
    public String getOutputBasename()
    {
        return outputBasename;
    }

    /**
     * Set the basename. Typically set to the value of the "-out" option.
     * 
     * @param outputBasename The base name of the output file.
     */
    public void setOutputBasename(String outputBasename)
    {
        this.outputBasename = outputBasename;
    }
    
    /**
     * Gets a boolean that indicates whether or not parallel code generation of
     * method bodies is enabled.
     * 
     * @return true if method bodies should be generated in parallel, false
     * otherwise.
     */
    public boolean getParallel()
    {
        return parallel;
    }

    /**
     * Enables or disables parallel code generation of method bodies.
     * 
     * @param parallel If true, method bodies will be generated in parallel.
     */
    public void setParallel(boolean parallel)
    {
        this.parallel = parallel;
    }

    /**
     * Gets a boolean that indicates whether or not inlining of
     * method bodies is enabled.
     * 
     * @return true if inlining enabled, false otherwise.
     */
    public boolean isInliningEnabled()
    {
        return enableInlining;
    }

    /**
     * Enables or disables parallel code generation of method bodies.
     * 
     * @param enableInlining If true, method bodies will be generated in parallel.
     */
    public void setEnableInlining(boolean enableInlining)
    {
        this.enableInlining = enableInlining;
    }

    /**
     * Enable or disable dead code removal.
     * @param removeDeadCode true if dead code is to be removed.
     */
    public void setRemoveDeadCode(boolean removeDeadCode)
    {
        this.removeDeadCode = removeDeadCode;
    }

    /**
     * @return true if dead code is to be removed.
     */
    public boolean getRemoveDeadCode()
    {
        return this.removeDeadCode;
    }
}
