/*
 *
 *  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.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.output.CountingOutputStream;

import org.apache.flex.tools.FlexTool;

import org.apache.royale.compiler.Messages;
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.CommandLineConfigurator;
import org.apache.royale.compiler.config.CompilerDiagnosticsConstants;
import org.apache.royale.compiler.config.Configuration;
import org.apache.royale.compiler.config.ConfigurationBuffer;
import org.apache.royale.compiler.config.ConfigurationPathResolver;
import org.apache.royale.compiler.config.ConfigurationValue;
import org.apache.royale.compiler.config.Configurator;
import org.apache.royale.compiler.config.ICompilerProblemSettings;
import org.apache.royale.compiler.config.ICompilerSettingsConstants;
import org.apache.royale.compiler.config.RSLSettings;
import org.apache.royale.compiler.config.RSLSettings.RSLAndPolicyFileURLPair;
import org.apache.royale.compiler.exceptions.ConfigurationException;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.common.Counter;
import org.apache.royale.compiler.internal.config.FlashBuilderConfigurator;
import org.apache.royale.compiler.internal.config.localization.LocalizationManager;
import org.apache.royale.compiler.internal.definitions.DefinitionBase;
import org.apache.royale.compiler.internal.graph.GraphMLWriter;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.projects.DefinitionPriority.BasePriority;
import org.apache.royale.compiler.internal.projects.RoyaleProjectConfigurator;
import org.apache.royale.compiler.internal.targets.LinkageChecker;
import org.apache.royale.compiler.internal.targets.SWFTarget;
import org.apache.royale.compiler.internal.targets.Target;
import org.apache.royale.compiler.internal.units.ResourceModuleCompilationUnit;
import org.apache.royale.compiler.internal.units.SourceCompilationUnitFactory;
import org.apache.royale.compiler.internal.units.StyleModuleCompilationUnit;
import org.apache.royale.compiler.internal.watcher.WatchThread;
import org.apache.royale.compiler.internal.watcher.WatchThread.IWatchWriter;
import org.apache.royale.compiler.internal.workspaces.Workspace;
import org.apache.royale.compiler.problems.ConfigurationProblem;
import org.apache.royale.compiler.problems.FileIOProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.InternalCompilerProblem;
import org.apache.royale.compiler.problems.UnableToBuildSWFProblem;
import org.apache.royale.compiler.projects.ICompilerProject;
import org.apache.royale.compiler.targets.ISWFTarget;
import org.apache.royale.compiler.targets.ITargetReport;
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.tree.as.IFileNode;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.compiler.units.ICompilationUnit.UnitType;
import org.apache.royale.swf.io.ISWFWriterFactory;
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.SizeReportWritingSWFWriter;
import org.apache.royale.utils.FilenameNormalization;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;

/**
 * The entry-point class for mxmlc.
 */
public class MXMLC implements FlexTool
{
    static final String NEWLINE = System.getProperty("line.separator");
    private static final String SWF_EXT = ".swf";
    private static final String DEFAULT_VAR = "file-specs";
    private static final String L10N_CONFIG_PREFIX = "org.apache.royale.compiler.internal.config.configuration";

    /**
     * Exit code enumerations.
     */
    public static enum ExitCode
    {
        // NOTE: Negative error codes do not work on OSX.
        // Therefore the following enum values must be non-negative.
        SUCCESS(0),
        PRINT_HELP(1),
        FAILED_WITH_ERRORS(2),
        FAILED_WITH_EXCEPTIONS(3),
        FAILED_WITH_CONFIG_ERRORS(4),
        WATCHING(1000);

        ExitCode(int code)
        {
            assert code >= 0 : "Exit code must be non-negative";
            this.code = code;
        }

        final int code;
        
        public int getCode()
        {
            return code;
        }
    }

    /**
     * Entry point for the <code>mxmlc</code> tool.
     * 
     * @param args Command line arguments.
     */
    public static void main(final String[] args)
    {
        final int exitCode = staticMainNoExit(args);
        if (exitCode != ExitCode.WATCHING.getCode())
        {
            System.exit(exitCode);
        }
    }
    
    /**
     * Entry point for the {@code <mxmlc>} Ant task.
     * 
     * @param args Command line arguments.
     * @return An exit code.
     */
    public static int staticMainNoExit(final String[] args)
    {
        final MXMLC mxmlc = new MXMLC();
        return mxmlc.mainNoExit(args);
    }
    
    /**
     * Determines whether an exit code should be considered
     * a fatal failure, such as for an Ant task.
     * 
     * @param code A numeric exit code.
     * @return <code>true</code> if the Ant task failed.
     */
    public static boolean isFatalFailure(final int code)
    {
        // This method really belongs in ExitCode
        // but that would complicate FlexTask.
        return code == ExitCode.FAILED_WITH_ERRORS.getCode() ||
               code == ExitCode.FAILED_WITH_EXCEPTIONS.getCode() ||
               code == ExitCode.FAILED_WITH_CONFIG_ERRORS.getCode();
    }

    @Override
    public String getName() {
        return FLEX_TOOL_MXMLC;
    }

    @Override
    public int execute(String[] args) {
        return mainNoExit(args);
    }


    /**
     * Entry point for when you already have an MXMLC instance.
     * This is for unit testing.
     * 
     * @param args Command line arguments.
     * @return An exit code.
     */
    public int mainNoExit(final String[] args)
    {
        return mainNoExit(args, System.err);
    }

    /**
     * Entry point for when you already have an MXML instance and want
     * to redirect <code>System.err</code>. This is for unit testing.
     * 
     * @param args Command line arguments.
     * @param err An {@link OutputStream} to use instead of <code>System.err</code>.
     * @return An exit code.
     */
    @SuppressWarnings("unused")
    public int mainNoExit(final String[] args, OutputStream err)
    {
        startTime = System.nanoTime();
        
        ExitCode exitCode = ExitCode.SUCCESS;
        try
        {
            final boolean continueCompilation = configure(args);
            boolean legacyOutput = config.useLegacyMessageFormat();
            CompilerProblemCategorizer categorizer = null;
            
            if (legacyOutput)
                categorizer = createProblemCategorizer();
            
            ProblemFormatter formatter = new WorkspaceProblemFormatter(workspace, categorizer); 
            
            ProblemPrinter printer = new ProblemPrinter(formatter, err);
            
            if (continueCompilation)
            {
                if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
                	System.out.println("Configuration is ok");
                project.setProblems(problems.getProblems());
                compile();
                exitCode = printProblems(printer, legacyOutput);
                reportTargetCompletion();
            }
            else if (problems.hasFilteredProblems())
            {
                if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
                	System.out.println("Failed with config errors");
                printer.printProblems(problems.getFilteredProblems());
                exitCode = ExitCode.FAILED_WITH_CONFIG_ERRORS;
            }
            else
            {
                exitCode = ExitCode.PRINT_HELP;
            }
        }
        catch (Exception e)
        {
            if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
            	System.out.println("Failed with exceptions");
            (new PrintStream(err)).println(e.getMessage());
            exitCode = ExitCode.FAILED_WITH_EXCEPTIONS;
        }
        finally
        {
            if (!config.getWatch() || !ExitCode.SUCCESS.equals(exitCode))
            {
                waitAndClose();
            }
            
            if (Counter.COUNT_TOKENS || Counter.COUNT_NODES ||
                Counter.COUNT_DEFINITIONS || Counter.COUNT_SCOPES)
            {
                Counter.getInstance().dumpCounts();
            }
        }
        if (config.getWatch() && ExitCode.SUCCESS.equals(exitCode))
        {
            setupWatcher();
            exitCode = ExitCode.WATCHING;
        }
        return exitCode.getCode();
    }

    protected void setupWatcher()
    {
        if (!config.getWatch())
        {
            return;
        }
        IWatchWriter writer = new IWatchWriter()
        {
            public void rebuild(Collection<ICompilationUnit> units, Collection<ICompilerProblem> problems) throws InterruptedException, IOException
            {
                startTime = System.nanoTime();
                workspace.startBuilding();
                try
                {
                    if (!setupTargetFile())
                    {
                        throw new IOException("Failed to setup target file.");
                    }
                    buildArtifact();
                    for (ICompilationUnit unit : units)
                    {
                        // call waitForBuildFinish() to ensure that binding data
                        // doesn't get lost when a new definition is created
                        unit.waitForBuildFinish(problems, null);
                    }
                }
                finally
                {
                    workspace.doneBuilding();
                }
            }
    
            public void write(Collection<ICompilationUnit> units) throws InterruptedException, IOException
            {
                workspace.startBuilding();
                try
                {
                    final File outputFile = new File(getOutputFilePath());
                    final int swfSize = writeSWF(swfTarget, outputFile);
                    long endTime = System.nanoTime();
                    String seconds = String.format("%5.3f", (endTime - startTime) / 1e9);
                    Map<String, Object> params = new HashMap<String, Object>();
                    params.put("byteCount", swfSize);
                    params.put("path", outputFile.getCanonicalPath());
                    params.put("seconds", seconds);
                    swfOutputMessage = Messages.getString("MXMLC.bytes_written_to_file_in_seconds_format",
                            params);
                    reportTargetCompletion();
                }
                finally
                {
                    workspace.doneBuilding();
                }
            }
        };
        WatchThread watcherThread = new WatchThread("SWF", writer, config, project, workspace, problems);
        watcherThread.start();
    }
    
    /**
     * Entry point for when you already have an MXML instance and just want to
     * compile and not link. This is for FB integration, but other IDEs could
     * use this too.
     * 
     * @param args Command line arguments.
     * @param err An {@link OutputStream} to use instead of <code>System.err</code>.
     * @return An exit code.
     */
    @SuppressWarnings("unused")
    public int mainCompileOnly(final String[] args, OutputStream err)
    {
        if (err == null)
            err = System.err;
        
        startTime = System.nanoTime();
        
        ExitCode exitCode = ExitCode.SUCCESS;
        try
        {
            final boolean continueCompilation = configure(args);
            boolean legacyOutput = config.useLegacyMessageFormat();
            CompilerProblemCategorizer categorizer = null;
            
            if (legacyOutput)
                categorizer = createProblemCategorizer();
            
            ProblemFormatter formatter = new WorkspaceProblemFormatter(workspace, categorizer); 
            
            ProblemPrinter printer = new ProblemPrinter(formatter, err);

            if (continueCompilation)
            {
                compile(true); // skip linking
                exitCode = printProblems(printer, legacyOutput);
                reportTargetCompletion();
            }
            else if (problems.hasFilteredProblems())
            {
                printer.printProblems(problems.getFilteredProblems());
                exitCode = ExitCode.FAILED_WITH_CONFIG_ERRORS;
            }
            else
            {
                exitCode = ExitCode.PRINT_HELP;
            }
        }
        catch (Exception e)
        {
            (new PrintStream(err)).println(e.getMessage());
            exitCode = ExitCode.FAILED_WITH_EXCEPTIONS;
        }
        finally
        {
            waitAndClose();
            
            if (Counter.COUNT_TOKENS || Counter.COUNT_NODES ||
                Counter.COUNT_DEFINITIONS || Counter.COUNT_SCOPES)
            {
                Counter.getInstance().dumpCounts();
            }
        }
        return exitCode.getCode();
    }

    /** 
     * Print the problems in either the legacy format or the new format.
     * 
     * @param printer
     * @param legacyOutput
     * @return ExitCode
     */
    private ExitCode printProblems(ProblemPrinter printer, boolean legacyOutput)
    {
        ExitCode exitCode = ExitCode.SUCCESS;
        
        if (legacyOutput)
        {
            if (printer.printProblems(problems.getFilteredProblems()) > 0)
            {
                if (problems.hasErrors())
                    exitCode = ExitCode.FAILED_WITH_ERRORS;      
                // no exit code for warnings because anything except 0 is
                // detected as a failure by various build tools 
            }
        }
        else
        {
            Collection<ICompilerProblem> errors = new ArrayList<ICompilerProblem>();
            Collection<ICompilerProblem> warnings = new ArrayList<ICompilerProblem>();
            
            problems.getErrorsAndWarnings(errors, warnings);
            
            int errorCount = errors.size();
            int warningCount = warnings.size();
            if (warningCount > 0)
            {
                System.err.println(Messages.getString("MXMLC.WarningsHeader"));
                printer.printProblems(warnings);                    

            }
            
            if (errorCount > 0)
            {
                System.err.println(Messages.getString("MXMLC.ErrorsHeader"));
                printer.printProblems(errors);
            }
            
            // Output summary of errors and warnings
            if (errorCount == 1)
                System.err.println(Messages.getString("MXMLC.1_error"));
            else if (errorCount > 0)
                System.err.println(Messages.getString("MXMLC.multiple_errors_format", 
                        Collections.<String,Object>singletonMap("errorCount", errors.size())));

            if (warningCount == 1)
                System.err.println(Messages.getString("MXMLC.1_warning"));
            else if (warningCount > 0)
                System.err.println(Messages.getString("MXMLC.multiple_warnings_format", 
                        Collections.<String,Object>singletonMap("warningCount", warnings.size())));
            
            if (errorCount > 0)
                exitCode = ExitCode.FAILED_WITH_ERRORS;
            // no exit code for warnings because anything except 0 is
            // detected as a failure by various build tools
        }
        
        return exitCode;
    }

    public MXMLC()
    {
        workspace = new Workspace();
        project = new RoyaleProject(workspace);
        problems = new ProblemQuery();
    }

    protected Workspace workspace;
    protected RoyaleProject project;
    public Configuration config;
    public ProblemQuery problems;
    public ConfigurationBuffer configBuffer;

	public Class<? extends Configuration> configurationClass = Configuration.class;
    protected Configurator projectConfigurator;

    protected ICompilationUnit mainCU;
    protected SWFTarget target;
    protected long startTime;     // start time of execution in nanoseconds
    protected ITargetSettings targetSettings;
    private ISWF swfTarget;
    private String swfOutputMessage;
    
    /**
     * Print a message.
     * 
     * @param msg Message text.
     */
    public void println(final String msg)
    {
        System.out.println(msg);
    }

    /**
     * Wait till the workspace to finish compilation and close.
     */
    protected void waitAndClose()
    {
        workspace.startIdleState();
        try
        {
            workspace.close();
        }
        finally
        {
            workspace.endIdleState(Collections.<ICompilerProject, Set<ICompilationUnit>>emptyMap());
        }
    }

    /**
     * Force terminate the compilation process.
     */
    protected void close()
    {
        workspace.close();
    }

    /**
     * Create a new Configurator. This method may be overridden to allow
     * Configurator subclasses to be created that have custom configurations.
     * 
     * @return a new instance or subclass of {@link Configurator}. 
     * 
     */
	protected Configurator createConfigurator()
    {
        return new RoyaleProjectConfigurator(configurationClass);
    }
    
    /**
     * Load configurations from all the sources.
     * 
     * @param args command line arguments
     * @return True if mxmlc should continue with compilation.
     */
    public boolean configure(final String[] args)
    {
        projectConfigurator = createConfigurator();
        
        try
        {
            // Print brief usage if no arguments provided.
            if (args.length == 0)
            {
                final String usage = CommandLineConfigurator.brief(
                        getProgramName(),
                        DEFAULT_VAR,
                        LocalizationManager.get(),
                        L10N_CONFIG_PREFIX);
                println(getStartMessage());
                if (usage != null)
                    println(usage);

                // Create a default configuration so we can exit gracefully.
                config = new Configuration();
                configBuffer = new ConfigurationBuffer(
                        Configuration.class, Configuration.getAliases());
                return false;
            }

            ConfigurationPathResolver resolver = new ConfigurationPathResolver(System.getProperty("user.dir")); 
            projectConfigurator.setConfigurationPathResolver(resolver);
            projectConfigurator.setWarnOnRoyaleOnlyOptionUsage(false);
            if (useFlashBuilderProjectFiles(args))
                projectConfigurator.setConfiguration(FlashBuilderConfigurator.computeFlashBuilderArgs(args, getTargetType().getExtension()), 
                                                        getConfigurationDefaultVariable());
            else
                projectConfigurator.setConfiguration(args, getConfigurationDefaultVariable());
            projectConfigurator.applyToProject(project);
            getTargetSettings();    // get targetSettings here to flush out any configuration problems.
            problems = new ProblemQuery(projectConfigurator.getCompilerProblemSettings());
            
            // Get the configuration and configBuffer which are now initialized.
            config = projectConfigurator.getConfiguration();
            Messages.setLocale(config.getToolsLocale());
            project.apiReportFile = config.getApiReport();
            configBuffer = projectConfigurator.getConfigurationBuffer();
            problems.addAll(projectConfigurator.getConfigurationProblems());

            // Print version if "-version" is present.
            if (configBuffer.getVar("version") != null)
            {
                println(VersionInfo.buildMessage());
                return false;
            }

            // Print help if "-help" is present.
            final List<ConfigurationValue> helpVar = configBuffer.getVar("help");
            if (helpVar != null)
            {
                processHelp(helpVar);
                return false;
            }
            
            for (String fileName : projectConfigurator.getLoadedConfigurationFiles())
            {
                println(Messages.getString("MXMLC.Loading_configuration_format", 
                        Collections.<String,Object>singletonMap("configurationName", fileName)));                
            }
            
            // Add a blank line between the configuration list and the rest of 
            // the output to make the start of the output easier to detect.
            println(""); 
            
            if (config.isVerbose())
            {
                for (final IFileSpecification themeFile : project.getThemeFiles())
                {
                    println(Messages.getString("MXMLC.Found_theme_file_format", 
                            Collections.<String, Object>singletonMap("themePath", 
                                    themeFile.getPath())));
                }
            }

            // If we have configuration errors then exit before trying to 
            // validate the target.
            if (problems.hasErrors())
                return false;
            
            validateTargetFile();

            DefinitionBase.setPerformanceCachingEnabled(!config.getWatch());

            return true;
        }
        catch (ConfigurationException e)
        {
            final ICompilerProblem problem = new ConfigurationProblem(e);
            problems.add(problem);
            return false;
        }
        catch (Exception e)
        {
            final ICompilerProblem problem = new ConfigurationProblem(null, -1, -1, -1, -1, e.getMessage());
            problems.add(problem);
            return false;
        }
    }

    /**
     * Get the default variable for this configuration. MXMLC has a default 
     * variable of "file-spec" and COMPC has default variable of 
     * "include-classes".
     * 
     * @return the default variable for the configuration.
     */
    protected String getConfigurationDefaultVariable()
    {
        return ICompilerSettingsConstants.FILE_SPECS_VAR;
    }
    
    private boolean useFlashBuilderProjectFiles(String[] args)
    {
        for (String arg : args)
        {
            if (arg.equals("-fb") || arg.equals("-use-flashbuilder-project-files"))
                return true;
        }
        return false;
    }
    
    /**
     * Validate target file.
     * 
     * @throws ConfigurationException
     */
    protected void validateTargetFile() throws ConfigurationException
    {
        if(mainCU instanceof ResourceModuleCompilationUnit)
            return; //when compiling a Resource Module, no target file is defined.
        
        final String targetFile = config.getTargetFile();
        if (targetFile == null)
            throw new ConfigurationException.MustSpecifyTarget(null, null, -1);

        final File file = new File(targetFile);
        if (!file.exists())
            throw new ConfigurationException.IOError(targetFile);
    }

    /**
     * Main body of this program. This method is called from the public static
     * method's for this program.
     * 
     * @return true if compiler succeeds
     */
    protected boolean compile()
    {
        return compile(false);
    }
    
    private boolean compile(boolean skipLinking)
    {
        boolean compilationSuccess = false;
        try
        {
            if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
            	System.out.println("Setting up target file");
            if (!setupTargetFile())
            {
                if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
                	System.out.println("Could not set up target file");
                return false;
            }
            
            if (config.isDumpAst())
                dumpAST();

            buildArtifact();
            project.generateAPIReport();

            if (swfTarget == null)
            {
                if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
                	System.out.println("No swftarget");
                return false;
            }
            
            // Don't create a swf if there are errors unless a 
            // developer requested otherwise.
            if (!config.getCreateTargetWithErrors() && problems.hasErrors())
            {
                if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
                	System.out.println("got errors creating target");
                return false;
            }

            if (skipLinking)
                return true;
            if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
            	System.out.println("attempting to write output");
            final File outputFile = new File(getOutputFilePath());
            final int swfSize = writeSWF(swfTarget, outputFile);
            long endTime = System.nanoTime();
            String seconds = String.format("%5.3f", (endTime - startTime) / 1e9);
            Map<String, Object> params = new HashMap<String, Object>();
            params.put("byteCount", swfSize);
            params.put("path", outputFile.getCanonicalPath());
            params.put("seconds", seconds);
            swfOutputMessage = Messages.getString("MXMLC.bytes_written_to_file_in_seconds_format",
                    params);
            dumpDependencyGraphIfNeeded();
            compilationSuccess = true;
        }
        catch (IOException e)
        {
            if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
            	System.out.println("got IOException in compile()");
            final FileIOProblem problem = new FileIOProblem(e);
            problems.add(problem);
        }
        catch (Exception e)
        {
            if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.COMPC_PHASES) == CompilerDiagnosticsConstants.COMPC_PHASES)
            	System.out.println("got Exception in compile()");
            final ICompilerProblem problem = new InternalCompilerProblem(e);
            problems.add(problem);
        }

        return compilationSuccess;
    }

    /**
     * Setup theme files.
     */
    protected void setupThemeFiles()
    {
        project.setThemeFiles(toFileSpecifications(config.getCompilerThemeFiles(), workspace));

        if (config.isVerbose())
        {
            for (final IFileSpecification themeFile : project.getThemeFiles())
            {
                verboseMessage(Messages.getString("MXMLC.Found_theme_file_format", 
                        Collections.<String,Object>singletonMap("themePath", themeFile.getPath())));
            }
        }
    }

    /**
     * Reports the size and location of the target that was created.
     * @throws InterruptedException 
     * 
     */
    protected void reportTargetCompletion() throws InterruptedException
    {
        if (swfOutputMessage != null)
        {
            reportRequiredRSLs(target);
            println(swfOutputMessage);
        }
    }

    /**
     * Set up any user defines customization of the problem severities.
     * 
     */
    private CompilerProblemCategorizer createProblemCategorizer()
    {
        ICompilerProblemSettings problemSettings = null;
        try
        {
            problemSettings = projectConfigurator.getCompilerProblemSettings();
        }
        catch (Exception e)
        {
            // Create a categorizer that will only use default settings.
        }

        return new CompilerProblemCategorizer(problemSettings);
    }

    /**
     * Parse all source files and dumpAST
     * 
     * @throws InterruptedException
     */
    private void dumpAST() throws InterruptedException
    {
        final List<String> astDump = new ArrayList<String>();
        final Collection<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();
        final ImmutableList<ICompilationUnit> compilationUnits = target.getReachableCompilationUnits(problems);
        for (final ICompilationUnit compilationUnit : compilationUnits)
        {
            final IASNode ast = compilationUnit.getSyntaxTreeRequest().get().getAST();
            if (ast != null)
            {
            	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.FILE_NODE) == CompilerDiagnosticsConstants.FILE_NODE)
            		System.out.println("MXMLC waiting for lock in populateFunctionNodes");
                ((IFileNode)ast).populateFunctionNodes();
            	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.FILE_NODE) == CompilerDiagnosticsConstants.FILE_NODE)
            		System.out.println("MXMLC done with lock in populateFunctionNodes");
                astDump.add(ast.toString());
            }
        }

        println(Joiner.on("\n\n").join(astDump));
    }

    /**
     * Build target artifact.
     * 
     * @throws InterruptedException threading error
     * @throws IOException IO error
     */
    protected void buildArtifact() throws InterruptedException, IOException
    {
        swfTarget = buildSWFModel();
    }

    /**
     * Build SWF model object and collect problems building SWF in
     * {@link #problems}.
     * 
     * @return SWF model or null if SWF can't be built.
     * @throws InterruptedException concurrency problem
     */
    private ISWF buildSWFModel() throws InterruptedException
    {
        final List<ICompilerProblem> problemsBuildingSWF =
                new ArrayList<ICompilerProblem>();
        final ISWF swf = target.build(problemsBuildingSWF);
        problems.addAll(problemsBuildingSWF);
        if (swf == null)
        {
            ICompilerProblem problem = new UnableToBuildSWFProblem(getOutputFilePath());
            problems.add(problem);
        }
        
        return swf;
    }

    private void reportRequiredRSLs(ISWFTarget target) throws InterruptedException
    {
        // Report the required RSLs:
        if (hasRSLs())
        {
            ITargetReport report = target.getTargetReport();
            
            if (report == null)
                return;     // target must not have been built.
            
            List<RSLSettings> requiredRSLs = report.getRequiredRSLs();
            List<String> legacyRSLs = targetSettings.getRuntimeSharedLibraries();
            
            if (requiredRSLs.isEmpty() && legacyRSLs.isEmpty())
                return;
            
            println(Messages.getString("MXMLC.Required_RSLs"));                
            
            // loop thru the RSLs and print out the required RSLs.
            for (RSLSettings rslSettings : requiredRSLs)
            {
                List<RSLAndPolicyFileURLPair>rslURLs = rslSettings.getRSLURLs();
                Map<String,Object> params = new HashMap<String,Object>();
                params.put("rslPath",rslURLs.get(0).getRSLURL());
                
                switch (rslURLs.size())
                {
                    case 0:
                        assert false; // One RSL URL is required.
                        break;
                    case 1:
                        println(Messages.getString("MXMLC.required_rsl_url_format", 
                                params));
                        break;
                    case 2:
                        println(Messages.getString("MXMLC.required_rsl_url_with_1_failover_format",
                                params));
                        break;
                    default:
                        params.put("failoverCount", rslURLs.size() - 1);
                        println(Messages.getString("MXMLC.required_rsl_url_with_multiple_failovers_format", 
                                params));
                        break;
                }

            }
            
            // All -runtime-shared-libraries are required
            for (String rslURL : legacyRSLs)
                println(Messages.getString("MXMLC.required_rsl_url_format", 
                        Collections.<String,Object>singletonMap("rslPath", rslURL)));
        }
    }

    /**
     * Virtual method that returns the type of target we are building.
     * Subclasses will override this method to return different target types.
     * 
     * @return The {@link TargetType} of the target we are building.
     */
    protected TargetType getTargetType()
    {
        return TargetType.SWF;
    }
    
    private ITargetSettings getTargetSettings()
    {
        if (targetSettings == null)
            targetSettings = projectConfigurator.getTargetSettings(getTargetType());
        
        return targetSettings;
    }

    private boolean hasRSLs()
    {
        return (getTargetSettings().getRuntimeSharedLibraryPath().size() > 0) ||
               (getTargetSettings().getRuntimeSharedLibraryPath().size() > 0);
    }

    /**
     * Write out SWF file and return file size in bytes.
     * 
     * @param swf SWF model
     * @param outputFile output SWF file handle
     * @return SWF file size in bytes
     * @throws IOException error
     */
    private int writeSWF(final ISWF swf, final File outputFile) throws IOException
    {
  
        final Header.Compression compression = Header.decideCompression(
                targetSettings.useCompression(), 
                targetSettings.getSWFVersion(),
                targetSettings.isDebugEnabled());
        final ISWFWriterFactory writerFactory = SizeReportWritingSWFWriter.getSWFWriterFactory(
                targetSettings.getSizeReport()); 
        final ISWFWriter writer = writerFactory.createSWFWriter(swf, compression,
                targetSettings.isDebugEnabled(), targetSettings.isTelemetryEnabled());
        
        return writer.writeTo(outputFile);
    }

    /**
     * MXMLC uses target file as the main compilation unit and derive the output
     * SWF file name from this file.
     *
     * @return true if successful, false otherwise.
     * 
     * @throws InterruptedException
     */
    protected boolean setupTargetFile() throws InterruptedException
    {
        final String mainFileName = config.getTargetFile();

        if(mainFileName != null)
        {
            final String normalizedMainFileName = FilenameNormalization.normalize(mainFileName);
            
            // Can not add a SourceHandler for *.css file because we don't want
            // to create compilation units for CSS files on the source path.
            if (mainFileName.toLowerCase().endsWith(".css"))
            {
                mainCU = new StyleModuleCompilationUnit(
                        project, 
                        workspace.getFileSpecification(normalizedMainFileName), 
                        BasePriority.SOURCE_LIST);
                // TODO: Use CSS file name once CSS module runtime code is finalized.
                config.setMainDefinition("CSSModule2Main");
                project.addCompilationUnitsAndUpdateDefinitions(
                        Collections.singleton(mainCU));
            }
            else
            {
                final SourceCompilationUnitFactory compilationUnitFactory =
                    project.getSourceCompilationUnitFactory();
                File normalizedMainFile = new File(normalizedMainFileName);
                if (compilationUnitFactory.canCreateCompilationUnit(normalizedMainFile))
                {
                    // Remove the main file from the source path and put it on the 
                    // source list. The only reason this needs to be done is to
                    // prevent the compilation unit of the main file from shadowing
                    // another compilation unit with the same qname. This can 
                    // happen in the odd case where you have test.mxml (main file)
                    // and test.as in the same directory and test.mxml's compilation
                    // unit end up shadowing test.as's cu.
                    project.removeSourceFile(normalizedMainFile);
                    project.addIncludeSourceFile(normalizedMainFile, true);
                    
                    Collection<ICompilationUnit> mainFileCompilationUnits =
                        workspace.getCompilationUnits(normalizedMainFileName, project);
                    
                    assert mainFileCompilationUnits.size() == 1;
                    mainCU = Iterables.getOnlyElement(mainFileCompilationUnits);
                }
            }
        }
        else 
        {
            final List<ICompilerProblem> resourceBundleProblems = new ArrayList<ICompilerProblem>();
            Collection<ICompilationUnit> includedResourceBundles = target.getIncludedResourceBundlesCompilationUnits(resourceBundleProblems);
            problems.addAll(resourceBundleProblems);

            if(includedResourceBundles.size() > 0)
            {
                //This means that a Resource Module is requested to be built.
                mainCU = new ResourceModuleCompilationUnit(project, 
                        "GeneratedResourceModule", 
                        includedResourceBundles, 
                        BasePriority.SOURCE_LIST);
                config.setMainDefinition("GeneratedResourceModule");
                project.addCompilationUnitsAndUpdateDefinitions(
                        Collections.singleton(mainCU));
            }
        }
                
        Preconditions.checkNotNull(mainCU, "Main compilation unit can't be null");

        if (getTargetSettings() == null)
            return false;            
        
        target = (SWFTarget)project.createSWFTarget(getTargetSettings(), null);
        
        return true;
    }

    /**
     * Setups the locale related settings.
     */
    protected void setupLocaleSettings()
    {
        project.setLocales(config.getCompilerLocales());
        project.setLocaleDependentResources(config.getLocaleDependentSources());
    }

    /**
     * Get the output file path. If {@code -output} is specified, use its value;
     * otherwise, use the same base name as the target file.
     * 
     * @return output file path
     */
    private String getOutputFilePath()
    {
        if (config.getOutput() == null)
            return FilenameUtils.removeExtension(config.getTargetFile()).concat(SWF_EXT);
        else
            return config.getOutput();
    }

    private void verboseMessage(String s)
    {
        if (config.isVerbose())
            println(s);
    }

    /**
     * Convert file path strings to {@code File} objects. Null values are
     * discarded.
     * 
     * @param paths list of paths.
     * @return List of File objects. No null values will be returned.
     */
    public static List<File> toFiles(final List<String> paths)
    {
        final List<File> result = new ArrayList<File>();
        for (final String path : paths)
        {
            if (path != null)
                result.add(new File(path));
        }
        return result;
    }

    /**
     * Resolve a list of normalized paths to {@link IFileSpecification} objects
     * from the given {@code workspace}.
     * 
     * @param paths A list of normalized paths.
     * @param workspace Workspace.
     * @return A list of file specifications.
     */
    public static List<IFileSpecification> toFileSpecifications(
            final List<String> paths,
            final Workspace workspace)
    {
        return Lists.transform(paths, new Function<String, IFileSpecification>()
        {
            @Override
            public IFileSpecification apply(final String path)
            {
            	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.WORKSPACE) == CompilerDiagnosticsConstants.WORKSPACE)
            		System.out.println("MXMLC waiting for lock in toFileSpecifications");
                IFileSpecification ret = workspace.getFileSpecification(path);
            	if ((CompilerDiagnosticsConstants.diagnostics & CompilerDiagnosticsConstants.WORKSPACE) == CompilerDiagnosticsConstants.WORKSPACE)
            		System.out.println("MXMLC waiting for lock in toFileSpecifications");
            	return ret;
            }
        });
    }

    /**
     * Get my program name.
     * 
     * @return always "mxmlc".
     */
    protected String getProgramName()
    {
        return "mxmlc";
    }

    /**
     * Get the start up message that contains the program name 
     * with the copyright notice.
     * 
     * @return The startup message.
     */
    protected String getStartMessage()
    {
        // This message should not be localized.
        String message = "Apache Royale MXML and ActionScript Compiler (mxmlc)" + NEWLINE +
            VersionInfo.buildMessage() + NEWLINE;
        return message;
    }

    /**
     * Print detailed help information if -help is provided.
     */
    private void processHelp(final List<ConfigurationValue> helpVar)
    {
        final Set<String> keywords = new LinkedHashSet<String>();
        for (final ConfigurationValue val : helpVar)
        {
            for (final Object element : val.getArgs())
            {
                String keyword = (String)element;
                while (keyword.startsWith("-"))
                    keyword = keyword.substring(1);
                keywords.add(keyword);
            }
        }

        if (keywords.size() == 0)
            keywords.add("help");

        final String usages = CommandLineConfigurator.usage(
                    getProgramName(),
                    DEFAULT_VAR,
                    configBuffer,
                    keywords,
                    LocalizationManager.get(),
                    L10N_CONFIG_PREFIX);
        println(getStartMessage());
        println(usages);
    }

    /**
     * "compc" subclass will override this method.
     * 
     * @return False if the client is not "compc".
     */
    protected boolean isCompc()
    {
        return false;
    }
    
    private void dumpDependencyGraphIfNeeded() throws IOException, InterruptedException
    {
        File dependencyGraphOutput = config.getDependencyGraphOutput();
        if (dependencyGraphOutput != null)
        {
            LinkageChecker linkageChecker = new LinkageChecker(project, getTargetSettings());
            Target.RootedCompilationUnits rootedCompilationUnits = target.getRootedCompilationUnits();
            problems.addAll(rootedCompilationUnits.getProblems());
            GraphMLWriter dependencyGraphWriter = 
                new GraphMLWriter(project.getDependencyGraph(), 
                        rootedCompilationUnits.getUnits(), true,
                        linkageChecker);
            BufferedOutputStream graphStream = new BufferedOutputStream(new FileOutputStream(dependencyGraphOutput));
            LinkedList<ICompilerProblem> problemList = new LinkedList<ICompilerProblem>();
            Iterables.addAll(problemList, rootedCompilationUnits.getProblems());
            dependencyGraphWriter.writeToStream(graphStream, problemList);
            problems.addAll(problemList);
        }
    }
    
    public ProblemQuery getProblems()
    {
        return problems;
    }

    public List<String> getSourceList()
    {
        ArrayList<String> list = new ArrayList<String>();
        LinkedList<ICompilerProblem> problemList = new LinkedList<ICompilerProblem>();
        try
        {
            ImmutableList<ICompilationUnit> units = target.getReachableCompilationUnits(problemList);
            for (ICompilationUnit unit : units)
            {
                UnitType ut = unit.getCompilationUnitType();
                if (ut == UnitType.AS_UNIT || ut == UnitType.MXML_UNIT)
                {
                    list.add(unit.getAbsoluteFilename());
                }
            }
        }
        catch (InterruptedException e)
        {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        
        return list;
    }
    
    public String getMainSource()
    {
    	if (mainCU == null) return "";
        return mainCU.getAbsoluteFilename();
    }
    
    public ISWF getSWFTarget()
    {
        return swfTarget;
    }
    
    public int writeSWF(OutputStream outputStream)
    {
        
        final Header.Compression compression = Header.decideCompression(
                targetSettings.useCompression(), 
                targetSettings.getSWFVersion(),
                targetSettings.isDebugEnabled());
        final ISWFWriterFactory writerFactory = SizeReportWritingSWFWriter.getSWFWriterFactory(
                targetSettings.getSizeReport()); 
        final ISWFWriter writer = writerFactory.createSWFWriter(swfTarget, compression,
                targetSettings.isDebugEnabled(), targetSettings.isTelemetryEnabled());
        
        // Write out the SWF, counting how many bytes were written.
        final CountingOutputStream output =
                new CountingOutputStream(outputStream);

        writer.writeTo(output);
        final int swfSize = output.getCount();
        return swfSize;
    }
}
