/*
 *
 *  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.internal.targets;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.ZipOutputStream;

import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;

import org.apache.royale.compiler.common.DependencyType;
import org.apache.royale.compiler.common.DependencyTypeSet;
import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.common.VersionInfo;
import org.apache.royale.compiler.common.XMLName;
import org.apache.royale.compiler.constants.IMetaAttributeConstants;
import org.apache.royale.compiler.definitions.IClassDefinition;
import org.apache.royale.compiler.definitions.IDefinition;
import org.apache.royale.compiler.definitions.metadata.IMetaTag;
import org.apache.royale.compiler.definitions.references.IResolvedQualifiersReference;
import org.apache.royale.compiler.definitions.references.ReferenceFactory;
import org.apache.royale.compiler.exceptions.BuildCanceledException;
import org.apache.royale.compiler.filespecs.IBinaryFileSpecification;
import org.apache.royale.compiler.internal.config.QNameNormalization;
import org.apache.royale.compiler.internal.filespecs.SWCFileSpecification;
import org.apache.royale.compiler.internal.projects.DependencyGraph;
import org.apache.royale.compiler.internal.projects.RoyaleProject;
import org.apache.royale.compiler.internal.projects.LibraryPathManager;
import org.apache.royale.compiler.internal.projects.ResourceBundleSourceFileHandler;
import org.apache.royale.compiler.internal.projects.SourcePathManager;
import org.apache.royale.compiler.internal.units.ResourceBundleCompilationUnit;
import org.apache.royale.compiler.problems.DuplicateScriptProblem;
import org.apache.royale.compiler.problems.FileNotFoundProblem;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.NoCompilationUnitForDefinitionProblem;
import org.apache.royale.compiler.problems.NoSourceForClassInNamespaceProblem;
import org.apache.royale.compiler.problems.NoSourceForClassProblem;
import org.apache.royale.compiler.targets.ISWCTarget;
import org.apache.royale.compiler.targets.ISWFTarget;
import org.apache.royale.compiler.targets.ITargetProgressMonitor;
import org.apache.royale.compiler.targets.ITargetReport;
import org.apache.royale.compiler.targets.ITargetSettings;
import org.apache.royale.compiler.units.ICompilationUnit;
import org.apache.royale.compiler.units.ICompilationUnit.UnitType;
import org.apache.royale.compiler.units.requests.IFileScopeRequestResult;
import org.apache.royale.swc.ISWC;
import org.apache.royale.swc.ISWCFileEntry;
import org.apache.royale.swc.ISWCLibrary;
import org.apache.royale.swc.ISWCManager;
import org.apache.royale.swc.ISWCVersion;
import org.apache.royale.swc.SWC;
import org.apache.royale.swc.SWCComponent;
import org.apache.royale.swc.SWCLibrary;
import org.apache.royale.swc.SWCScript;
import org.apache.royale.swf.ISWF;
import org.apache.royale.utils.DAByteArrayOutputStream;
import org.apache.royale.utils.FileID;

import com.google.common.base.Function;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;

/**
 * Compilation target for SWC library.
 */
public class SWCTarget extends Target implements ISWCTarget
{
    private static final String LIBRARY_SWF = "library.swf";

    public SWCTarget(final RoyaleProject project, ITargetSettings targetSettings, ITargetProgressMonitor progressMonitor)
    {
        super(project, targetSettings, progressMonitor);
        swc = new SWC(targetSettings.getOutput());
        royaleProject = project;
    }

    private final SWC swc;
    private final RoyaleProject royaleProject;
    private ILibrarySWFTarget librarySWFTarget;
    
    private RootedCompilationUnits rootedCompilationUnits;

    @Override
    public ISWC build(final Collection<ICompilerProblem> problems)
    {
        buildStarted();
        try
        {
            Iterable<ICompilerProblem> fatalProblems = getFatalProblems();
            if (!Iterables.isEmpty(fatalProblems))
            {
                Iterables.addAll(problems, fatalProblems);
                return null;
            }

            setVersionInfo();
            final HashSet<IDefinition> definitions = new HashSet<IDefinition>();
            buildLibrarySWF(definitions, problems);
            addComponents(definitions);
            addFileEntriesToSWC(problems);

            return swc;
        }
        catch (BuildCanceledException bce)
        {
            return null;
        }
        catch (InterruptedException ie)
        {
            return null;
        }
        finally
        {
            buildFinished();
        }
    }
    
    /**
     * Sets version information to be written into the <versions> tag in catalog.xml.
     */
    private void setVersionInfo()
    {
        ISWCVersion swcVersion = swc.getVersion();
        swcVersion.setSWCVersion(VersionInfo.getLibVersion());
        if (royaleProject.isRoyale())
        {
            swcVersion.setRoyaleVersion(VersionInfo.getRoyaleVersion());
            swcVersion.setRoyaleBuild(VersionInfo.getBuild());
            swcVersion.setRoyaleMinSupportedVersion(targetSettings.getRoyaleMinimumSupportedVersion());
        }
        swcVersion.setCompilerName(VersionInfo.getCompilerName());
        swcVersion.setCompilerVersion(VersionInfo.getCompilerVersion());
        swcVersion.setCompilerBuild(VersionInfo.getCompilerBuild());
    }
    
    @Override
    public ISWFTarget getLibrarySWFTarget() throws InterruptedException
    {
        if (librarySWFTarget == null)
        {
            Target.RootedCompilationUnits rootedCompilationUnits = getRootedCompilationUnits();
            if (royaleProject.isRoyale())
            {
                librarySWFTarget =
                    new RoyaleLibrarySWFTarget(royaleProject, targetSettings, rootedCompilationUnits.getUnits());                
            }
            else
            {
                librarySWFTarget = new LibrarySWFTarget(royaleProject, targetSettings,
                        rootedCompilationUnits.getUnits());                
            }
        }
        assert librarySWFTarget != null;
        return librarySWFTarget;
    }

    @Override
    public TargetType getTargetType()
    {
        return TargetType.SWC;
    }

    /**
     * Creating {@code <component>} tags in catalog.xml.
     * 
     * @param definitions All definitions to be linked into the target.
     */
    private void addComponents(final HashSet<IDefinition> definitions)
    {
        final Set<String> includedNamespaces = ImmutableSet.copyOf(targetSettings.getIncludeNamespaces());
        
        for (String namespace : includedNamespaces)
        {
            // For each namespace get the set of classes. 
            // From the classes get the the compilation units.
            // Validate the compilation units are resolved to source
            // files unless there are lookupOnly entries.
            final Collection<String> includeNamespaceQualifiedNames =
                royaleProject.getQualifiedClassNamesForManifestNamespaces(
                        Collections.singleton(namespace));
            for (String qName : includeNamespaceQualifiedNames)
            {
                final Collection<XMLName> tagNames = royaleProject.getTagNamesForClass(qName);
                for (XMLName tagName : tagNames)
                {
                    if (includeComponent(tagName, includedNamespaces))
                    {
                        final SWCComponent component = new SWCComponent();
                        component.setName(tagName.getName());
                        component.setURI(tagName.getXMLNamespace());
                        component.setQName(qName);
                        swc.addComponent(component);                    
                    }
                }                
            }
        }

        /*
        for (final IDefinition def : definitions)
        {
            final String qName = def.getQualifiedName();
            
            final Collection<XMLName> tagNames = royaleProject.getTagNamesForClass(qName);
            for (XMLName tagName : tagNames)
            {
                if (includeComponent(tagName, includedNamespaces))
                {
                    final SWCComponent component = new SWCComponent();
                    component.setName(tagName.getName());
                    component.setURI(tagName.getXMLNamespace());
                    component.setQName(qName);
                    swc.addComponent(component);                    
                }
            }
        }
        */
    }

    /**
     * Test if a component should be included in a SWC's component list.
     * <p>
     * The rule for including a component in a SWC's component list is that
     * the component must be in one of the SWC's included namespaces
     * (meaning those specified by -include-namespace, not -namespace)
     * and either the component's lookupOnly flag is false
     * or the option -include-lookup-only is true.
     * 
     * @param tagName the tag name of the component to check.
     * @param includedNamespaces the namespaces included in this SWC using the
     * -include-namespaces option.
     * @return true if component should be included, false otherwise.
     */
    private boolean includeComponent(XMLName tagName, Set<String> includedNamespaces)
    {
        if (includedNamespaces.contains(tagName.getXMLNamespace()))
        {
            return !royaleProject.isManifestComponentLookupOnly(tagName) ||
                   targetSettings.isIncludeLookupOnlyEnabled();
        }
        
        return false;
     }

    /**
     * Add asset files to the SWC model.
     * 
     * @param problems Problem collection.
     */
    private void addFileEntriesToSWC(Collection<ICompilerProblem> problems)
    {
        // -include
        Map<String, FileEntryValue> fileEntries = computeIncludedFiles();
        for (Entry<String, FileEntryValue> entry : fileEntries.entrySet())
        {
            processFileEntry(entry, problems);
        }
        
        // -include-libraries
        Map<String, ISWCFileEntry> includeLibraryFiles = getIncludedLibrariesFiles();
        for (ISWCFileEntry fileEntry : includeLibraryFiles.values())
        {
            swc.addFile(fileEntry);
        }

        // [IconFile(...)] metadata
        Map<String, FileEntryValue> iconFilesMap = computeIconFiles();
        for (Entry<String, FileEntryValue> entry : iconFilesMap.entrySet())
        {
            processFileEntry(entry, problems);
        }
    }
    
    /**
     * Helper method used by addFileEntriesToSWC() to process one file.
     */
    private void processFileEntry(Entry<String, FileEntryValue> entry,
                                  Collection<ICompilerProblem> problems)
    {
        String path = entry.getKey();
        IBinaryFileSpecification fileSpec = entry.getValue().getFileSpec();
        ISourceLocation sourceLocation = entry.getValue().getSourceLocation();
        
        byte[] contents = getContents(fileSpec);
        
        if (contents != null)
        {
            swc.addFile(path, fileSpec.getLastModified(), contents);
        }
        else    
        {
            final ICompilerProblem problem =
                sourceLocation != null ?
                new FileNotFoundProblem(sourceLocation, fileSpec.getPath()) :
                new FileNotFoundProblem(fileSpec.getPath());
            problems.add(problem);
        }
    }
    
    /**
     * Returns the contents of a binary file as an array of bytes,
     * or null if the file cannot be read.
     */
    private byte[] getContents(IBinaryFileSpecification fileSpec)
    {
        byte[] contents = null;
        
        try
        {
            final DAByteArrayOutputStream buffer = new DAByteArrayOutputStream();
            final InputStream fileInputStream = fileSpec.createInputStream();
            IOUtils.copy(fileInputStream, buffer);
            IOUtils.closeQuietly(buffer);
            IOUtils.closeQuietly(fileInputStream);
            contents = buffer.getDirectByteArray();
        }
        catch (IOException e)
        {
        }
        
        return contents;
    }
    
    /**
     * Build "library.swf" model and add to the SWC model as a
     * {@link ISWCLibrary}.
     * 
     * @param definitionsToBuild After this method returns, all the definitions
     * to be build into the target will be stored on this collection. It is
     * needed to compute a set of URLs for {@code <component>} tags in
     * catalog.xml file.
     * @param problems Compiler problems.
     * @return the ISWF for the library
     * @throws InterruptedException Compilation terminated.
     */
    private ISWF buildLibrarySWF(final Collection<IDefinition> definitionsToBuild,
            final Collection<ICompilerProblem> problems)
            throws InterruptedException
    {
        getLibrarySWFTarget();
        Iterables.addAll(problems, getRootedCompilationUnits().getProblems());
        
        final LinkageChecker externalLinkageChecker = new LinkageChecker(
                royaleProject, targetSettings);
        ((Target)librarySWFTarget).setLinkageChecker(externalLinkageChecker);
        setLinkageChecker(externalLinkageChecker);
        final ISWF defaultLibrarySWF = librarySWFTarget.build(problems);
        
        // make default library model
        final ISWCLibrary defaultLibrary = new SWCLibrary(LIBRARY_SWF, defaultLibrarySWF);
        swc.addLibrary(defaultLibrary);
    
        // Deal with all the cu's that don't need to be added via addScript
        // save all the add script ones in cuToWrite for the next phase.
        Set<ICompilationUnit> cuToWrite = new HashSet<ICompilationUnit>();
        for (final ICompilationUnit cu : librarySWFTarget.getCompilationUnits())
        {
            if (isLinkageExternal(cu, targetSettings))
            {
                    // don't do anything with these
            }
            //Resource bundles processed uniquely
            else if (cu instanceof ResourceBundleCompilationUnit)
            {
                assert project instanceof RoyaleProject;
                processResourceBundle((RoyaleProject)project, (ResourceBundleCompilationUnit)cu,
                            swc, problems);
            }
            else
            {
                // everyone else goes in this list for next step             
                cuToWrite.add(cu);
            }
        }
        
        // remove duplicates and log resulting problems, then add to library
        filterCompilationUnits(cuToWrite, problems);   
        for (final ICompilationUnit cu : cuToWrite)
        {
            defaultLibrary.addScript(createScript(cu, definitionsToBuild));
        }
        
        // Add the generated root class and its dependencies to the list of 
        // scripts.
        if (librarySWFTarget.getRootClassName() != null)
        {
            final SWCScript script = new SWCScript();
            script.setName(librarySWFTarget.getRootClassName());
            script.addDefinition(librarySWFTarget.getRootClassName());
            // This class is synthetic and is supposed to be globally
            // unique, so nobody should care what the time stamp is.
            // If we set the time stamp to the current time, then
            // every SWC we make will be slightly different from all other
            // SWCs.
            script.setLastModified(1);

            // add baseclass as an inheritance dependency
            script.addDependency(librarySWFTarget.getBaseClassQName(), DependencyType.INHERITANCE);
            defaultLibrary.addScript(script);
        }
        
        if (librarySWFTarget.getASMetadataNames() != null)
        {
            for (String name : librarySWFTarget.getASMetadataNames())
            {
                defaultLibrary.addNameToKeepAS3MetadataSet(name);
            }
        }

        return defaultLibrarySWF;
    }

    /**
     * removes compilation units from a list if they are shaddowed by other ones
     * that try to generate the same script. Logs a problem in this case
     * 
     * @param cuToWrite
     * @param problems
     * @throws InterruptedException
     */
    private void filterCompilationUnits(Set<ICompilationUnit> cuToWrite, Collection<ICompilerProblem> problems) throws InterruptedException
    {
        // group cu's by script name.
        Map<String, Set<ICompilationUnit>> cuMap = new HashMap<String, Set<ICompilationUnit>>();
        
        Comparator<ICompilationUnit> comparator = new Comparator<ICompilationUnit>()   {
            @Override
            public int compare(ICompilationUnit arg0, ICompilationUnit arg1) {
                // inverted sort on name will give the behavior of the old compiler:
                // last path in alphabetical order wins
                return -arg0.getName().compareTo(arg1.getName()); }};
                
        for (ICompilationUnit cu : cuToWrite)
        {
            String name = cu.getSWFTagsRequest().get().getDoABCTagName();
            Set<ICompilationUnit> l = cuMap.get(name);
            if (l == null)
            {
                l = new TreeSet<ICompilationUnit>(comparator);
                cuMap.put(name, l);
            }
            l.add(cu);
        }

        // now that we have grouped CUs based on script name, we can look for dupes
        for (Collection<ICompilationUnit> cus : cuMap.values())
        {
            assert !cus.isEmpty();

            Iterator<ICompilationUnit> it = cus.iterator();

            ICompilationUnit cuKept = it.next(); // we are keeping the first one
            ICompilationUnit cuDumped = null;
            boolean somoneHasVisibleDefinitions = hasExternallyVisibleDefinitions(cuKept);

            while (it.hasNext()) // remove all the dupes from the list
            {
                cuDumped = it.next(); // Remember the last dupe to report it
                cuToWrite.remove(cuDumped);
                somoneHasVisibleDefinitions |= hasExternallyVisibleDefinitions(cuDumped);
            }

            // report a problem for dupe, unless none of the shadows has anything in it.
            if (cuDumped != null && somoneHasVisibleDefinitions)
            {
                problems.add(new DuplicateScriptProblem(cuKept.getAbsoluteFilename(), cuDumped.getAbsoluteFilename()));
            }
        }
    }
    
    private boolean hasExternallyVisibleDefinitions(ICompilationUnit cu) throws InterruptedException
    {
        final IFileScopeRequestResult r = cu.getFileScopeRequest().get();
        Collection<IDefinition> vis = r.getExternallyVisibleDefinitions();
        return !vis.isEmpty(); 
    }

    /**
     * Create a new script based on the compilation unit.
     * 
     * @param cu
     * @param definitionsToBuild
     * @return
     * @throws InterruptedException
     */
    private SWCScript createScript(ICompilationUnit cu, 
            Collection<IDefinition> definitionsToBuild) throws InterruptedException
    {
        // Create a script model per compilation unit.
        final SWCScript script = new SWCScript();
        script.setName(cu.getSWFTagsRequest().get().getDoABCTagName());

        // Add all the externally visible definitions to the script model.
        final IFileScopeRequestResult fsResult = cu.getFileScopeRequest().get();
        for (final IDefinition def : fsResult.getExternallyVisibleDefinitions())
        {
            script.addDefinition(def.getQualifiedName());
            script.setLastModified(cu.getSyntaxTreeRequest().get().getLastModified());
            
            if (definitionsToBuild != null)
                definitionsToBuild.add(def);
        }
        final DependencyGraph dependencyGraph =
            royaleProject.getDependencyGraph();
        Set<ICompilationUnit> directDependencies = 
            dependencyGraph.getDirectDependencies(cu);
        for (ICompilationUnit directDependency : directDependencies)
        {
            final Map<String, DependencyTypeSet> dependenciesMap =
                dependencyGraph.getDependencySet(cu, directDependency);
            for (Map.Entry<String, DependencyTypeSet> dependencyEntry : dependenciesMap.entrySet())
            {
                for (DependencyType type : dependencyEntry.getValue())
                    script.addDependency(dependencyEntry.getKey(), type);
            }
            
        }

        return script;
    }
    
    /**
     * Process a resource bundle represented by the specified compilation unit.
     * 
     * @param project active project
     * @param cu resource bundle compilation unit to process
     * @param swc target swc
     * @param problems problems collection
     */
    private void processResourceBundle(RoyaleProject project, ResourceBundleCompilationUnit cu,
            SWC swc, Collection<ICompilerProblem> problems)
    {
        Collection<String> locales = null;
        if (cu.getLocale() == null)
        {
            //Create a file entry for each locale since this compilation unit is not locale specific
            locales = project.getLocales();
        }
        else
        {
            //This compilation unit is for a locale specific file, 
            //therefore create a file entry for only the locale comp unit depends on
            locales = Collections.singletonList(cu.getLocale());
        }

        byte[] fileContent = cu.getFileContent(problems); //get file content

        if (fileContent == null)
            return; //Is this the right thing to do here?

        for (String locale : locales)
        {
            //Build the destination path. Destination path for a .properties file 
            //is such as locale/{locale}/foo/bar/x.properties

            StringBuilder sb = new StringBuilder();

            sb.append(ResourceBundleCompilationUnit.LOCALE);
            sb.append(File.separator);
            sb.append(locale);
            sb.append(File.separator);
            sb.append(QNameNormalization.normalize(cu.getBundleNameInColonSyntax()).replace('.', File.separatorChar));
            sb.append(FilenameUtils.EXTENSION_SEPARATOR_STR);
            sb.append(ResourceBundleSourceFileHandler.EXTENSION);

            swc.addFile(sb.toString(), cu.getFileLastModified(), fileContent);
        }
    }

    @Override
    public boolean addToZipOutputStream(ZipOutputStream output, Collection<ICompilerProblem> problemCollection)
    {
        throw new UnsupportedOperationException();
    }

    @Override
    protected Target.RootedCompilationUnits computeRootedCompilationUnits() throws InterruptedException
    {
        final Set<ICompilationUnit> rootCompilationUnits = new HashSet<ICompilationUnit>();

        final Collection<File> includedSourceFiles = targetSettings.getIncludeSources();
        final Set<String> includeClassNameSet = ImmutableSet.copyOf(targetSettings.getIncludeClasses());
        final Set<String> includedNamespaces = ImmutableSet.copyOf(targetSettings.getIncludeNamespaces());

        final ArrayList<ICompilerProblem> problems = new ArrayList<ICompilerProblem>();
        
        // Select definitions according to configurations.

        // include-namespace
        final Collection<ICompilationUnit> includeNamespaceUnits = 
            getCompilationUnitsForIncludedNamespaces(includedNamespaces, problems); 
        rootCompilationUnits.addAll(includeNamespaceUnits);
        
        // include-class + include-namespace
        rootCompilationUnits.addAll(getCompilationUnitsFromClassNames(null, includeClassNameSet, problems));

        // include-source
        for (final File includedSourceFileName : includedSourceFiles)
        {
            // Get all the compilation units in the project that reference the specified file.
            Collection<ICompilationUnit> compilationUnitsForFile = project.getWorkspace().getCompilationUnits(includedSourceFileName.getAbsolutePath(), project);
            
            // For compilation units with that differ by qname, choose the compilation that
            // appears first on the source-path.
            if (compilationUnitsForFile.size() > 1)
            {
                compilationUnitsForFile = filterUnitsBasedOnSourcePath(compilationUnitsForFile);
            }
            
            for (ICompilationUnit cu : compilationUnitsForFile)
            {
                // IFilter out any compilation unit in the list where the specified file is not the root
                // source file compiled by the compilation unit.
                if (cu.getAbsoluteFilename().equals(includedSourceFileName.getAbsolutePath()))
                    rootCompilationUnits.add(cu);
            }
        }

        //Add compilation units for included resource bundles
        for (ICompilationUnit rbCompUnit : getIncludedResourceBundlesCompilationUnits(problems))
            rootCompilationUnits.add(rbCompUnit);

        // -include and -include-libraries
        rootCompilationUnits.addAll(getIncludesCompilationUnits());
        rootCompilationUnits.addAll(getIncludeLibrariesCompilationUnits());
        
        return new Target.RootedCompilationUnits(rootCompilationUnits, problems);
    }
    
    @Override
    public RootedCompilationUnits getRootedCompilationUnits() throws InterruptedException
    {
        if (this.rootedCompilationUnits == null)
            rootedCompilationUnits = computeRootedCompilationUnits();
        assert rootedCompilationUnits != null;
        return rootedCompilationUnits;
    }
    

    /**
     * For compilation units with the same absolute source path, filter based on
     * the source path. The compilation unit found on the highest priority
     * source path wins. The rest of the compilation units with qnames are
     * discared. If a unit is not on the source path or does not have a qname or
     * more than one qname, then let it thru the filter.
     * 
     * @param compilationUnitsForFile list of compilation units to filter.
     * @return filtered compilation units.
     * @throws InterruptedException
     */
    private Collection<ICompilationUnit> filterUnitsBasedOnSourcePath(Collection<ICompilationUnit> compilationUnitsForFile) throws InterruptedException
    {
        List<ICompilationUnit> sourcePathUnits = new ArrayList<ICompilationUnit>(compilationUnitsForFile);
        boolean foundHighestPriorityUnit = false;
        for (File sourcePath : royaleProject.getSourcePath())
        {
            for (ICompilationUnit unit : sourcePathUnits)
            {
                // We only care about filtering units on the source path
                // that follow the single definition rule.
                UnitType unitType = unit.getCompilationUnitType();
                if (unitType == UnitType.AS_UNIT || unitType == UnitType.FXG_UNIT ||
                    unitType == UnitType.MXML_UNIT || unitType == UnitType.CSS_UNIT)
                {
                    Collection<String> qnames = unit.getQualifiedNames();
                    if (qnames.size() > 1)
                        continue;
                    
                    String unitQname = qnames.isEmpty() ? "" : qnames.iterator().next();
                    String computedQname = SourcePathManager.computeQName(sourcePath, new File(unit.getAbsoluteFilename()));
                    
                    if (unitQname.equals(computedQname))
                    {
                        // We found a unit on the source path. Only keep the 
                        // first unit found on the source path and remove the 
                        // others.
                        if (foundHighestPriorityUnit)
                            compilationUnitsForFile.remove(unit);
                        
                        foundHighestPriorityUnit = true;
                        break; // should only be one compilation unit on a source path
                    }
                }
            }
        }
        
        return compilationUnitsForFile;
    }

    /**
     * Get the compilation units for the given included namespaces. Also perform error
     * checking.
     * 
     * @param namespaces the namespaces included in this swc target.
     * @param problems A collection where detected problems are added.
     * @return A collection of compilation units.
     * @throws InterruptedException 
     */
    private Collection<ICompilationUnit> getCompilationUnitsForIncludedNamespaces(
            Collection<String> namespaces,
            Collection<ICompilerProblem> problems) throws InterruptedException
    {
        final Collection<ICompilationUnit> allUnits = new HashSet<ICompilationUnit>();
        
        for (String namespace : namespaces)
        {
            // For each namespace get the set of classes. 
            // From the classes get the the compilation units.
            // Validate the compilation units are resolved to source
            // files unless there are lookupOnly entries.
            final Collection<String> includeNamespaceQualifiedNames =
                royaleProject.getQualifiedClassNamesForManifestNamespaces(
                        Collections.singleton(namespace));
            final Collection<ICompilationUnit> units = 
                getCompilationUnitsFromClassNames(namespace, includeNamespaceQualifiedNames, problems);
            validateIncludeNamespaceEntries(namespace, units, problems);
            allUnits.addAll(units);
        }
        return allUnits;
    }
    
    /**
     * Validate that the manifest entries in the included namespaces resolve to
     * source files, not classes from other SWCs. The exception is for entries
     * that are "lookupOnly".
     * 
     * @param namespace The target namespace.
     * @param units The compilation units found in that namespace.
     * @param problems detected problems are added to this list.
     * @throws InterruptedException 
     */
    private void validateIncludeNamespaceEntries(String namespace, 
            Collection<ICompilationUnit> units, 
            Collection<ICompilerProblem> problems) throws InterruptedException
    {
        for (ICompilationUnit unit : units)
        {
            List<String> classNames = unit.getQualifiedNames();
            String className = classNames.get(classNames.size() - 1);
            Collection<XMLName> xmlNames = royaleProject.getTagNamesForClass(className);
            for (XMLName xmlName : xmlNames)
            {
                if (namespace.equals(xmlName.getXMLNamespace()))
                {
                    if (!royaleProject.isManifestComponentLookupOnly(xmlName) && 
                        unit.getCompilationUnitType() == UnitType.SWC_UNIT)
                    {
                        problems.add(new NoSourceForClassInNamespaceProblem(namespace, className));
                    }
                    break;
                }
            }
        }
    }

    /**
     * Return a collection of compilation units for a collection of class names. 
     *
     * @param namespace the namespace of the classes. Null if there is no namespace.
     * @param classNames
     * @param problems detected problems are added to this list.
     * @return a collection of compilation units.
     */
    private Collection<ICompilationUnit> getCompilationUnitsFromClassNames(String namespace,
            Collection<String> classNames,
            final Collection<ICompilerProblem> problems)
    {
        Collection<String> compilableClassNames = new ArrayList<String>();
        for (String className : classNames)
        {
            Collection<XMLName> tagNames = royaleProject.getTagNamesForClass(className);
            boolean okToAdd = true;
            for (XMLName tagName : tagNames)
            {
                if (royaleProject.isManifestComponentLookupOnly(tagName))
                    okToAdd = false;
            }
            if (okToAdd)
                compilableClassNames.add(className);
        }
        
        // Class names are turned into references and then info compilation units.
        final Iterable<IResolvedQualifiersReference> references = 
            Iterables.transform(compilableClassNames, new Function<String, IResolvedQualifiersReference>()
                {
                    @Override
                    public IResolvedQualifiersReference apply(String qualifiedName)
                    {
                        return ReferenceFactory.packageQualifiedReference(project.getWorkspace(), qualifiedName, true);
                    }
                });

        Collection<ICompilationUnit> units = new LinkedList<ICompilationUnit>();
        for (IResolvedQualifiersReference reference : references)
        {
            IDefinition def = reference.resolve(royaleProject);
            if (def == null)
            {
                if (namespace == null)
                    problems.add(new NoSourceForClassProblem(reference.getDisplayString()));
                else
                    problems.add(new NoSourceForClassInNamespaceProblem(namespace, reference.getDisplayString()));
            }
            else
            {
                ICompilationUnit defCU = project.getScope().getCompilationUnitForDefinition(def);
                if (defCU == null)
                    problems.add(new NoCompilationUnitForDefinitionProblem(def.getBaseName()));
                else
                    units.add(defCU);
            }
        }

        return units;
    }

    /**
     * @return Expand {@code -include-files} items into normalized file paths.
     */
    private Map<String, FileEntryValue> computeIncludedFiles()
    {
        Map<String, FileEntryValue> includedFiles = new HashMap<String, FileEntryValue>();
        for (Entry<String, File> entry : targetSettings.getIncludeFiles().entrySet())
        {
            String filename = entry.getKey();
            if (filename.contains("*"))
            {
                Collection<File> files = getFiles(entry.getValue());
                String basename = entry.getValue().getAbsolutePath();
                for (File file : files)
                {
                    String path = file.getAbsolutePath();
                    IBinaryFileSpecification fileSpec = project.getWorkspace().getLatestBinaryFileSpecification(path);

                    if (filename != null && fileSpec != null)
                    {
                        String relativePath = path.substring(basename.length() + 1);
                        relativePath = filename.replace("*", relativePath);
                        FileEntryValue value = new FileEntryValue(fileSpec, null);
                        includedFiles.put(relativePath, value);
                    }                    
                }
            }
            else
            {
                String path = entry.getValue().getAbsolutePath();
                IBinaryFileSpecification fileSpec = project.getWorkspace().getLatestBinaryFileSpecification(path);
    
                if (filename != null && fileSpec != null)
                {
                    FileEntryValue value = new FileEntryValue(fileSpec, null);
                    includedFiles.put(filename, value);
                }
            }
        }

        return includedFiles;
    }
    
    private Collection<File> getFiles(File file)
    {
        String filename = file.getAbsolutePath();
        if (filename.endsWith(File.separator))
            filename = filename.substring(0, filename.length() - 1);
        Collection<File> files = FileUtils.listFiles(new File(
                filename), null, true);
        return files;
    }
    /**
     * The collection of files from all of the libraries found on the 
     * -include-libraries path.
     * 
     * @return collection of absolute path names of SWC files.
     */
    private Map<String, ISWCFileEntry> getIncludedLibrariesFiles()
    {
        Map<String, ISWCFileEntry> files = new HashMap<String, ISWCFileEntry>();
        
        // Find all of the libraries on the -include-library path
        Set<FileID> swcs = LibraryPathManager.discoverSWCFilePaths(
                targetSettings.getIncludeLibraries().toArray(new File[0]));
        
        // For each library, get a compilation unit for every class in the
        // library.
        ISWCManager swcManager = project.getWorkspace().getSWCManager();
        for (FileID swcPath : swcs)
        {
            ISWC swc = swcManager.get(swcPath.getFile());
            Map<String, ISWCFileEntry> swcFiles = swc.getFiles();
            for (Map.Entry<String, ISWCFileEntry> file : swcFiles.entrySet())
            {
                ISWCFileEntry currentFileEntry = files.get(file.getKey());
                if (currentFileEntry == null ||
                   (file.getValue().getLastModified() > currentFileEntry.getLastModified()))
                {
                    files.put(file.getKey(), file.getValue());
                }
            }
        }  
        
        return files;
    }
    
    /**
     * Loops over all externally visible definitions in all compilation units
     * for this target, looking for class definitions with [IconFile(...)] metadata.
     * <p>
     * Suppose we find [IconFile("MyComponent.png")] on the class foo.bar.MyComponent.
     * Then the returned map will have an entry whose key is "foo/bar/MyComponent"
     * and whose value is a file specification that can be used to read the contents
     * of this file. A file with the same contents will get created at foo/bar/MyComponent
     * inside the SWC.
     */
    private Map<String, FileEntryValue> computeIconFiles()
    {
        Map<String, FileEntryValue> iconFiles = new HashMap<String, FileEntryValue>();

        try
        {
            Collection<ICompilationUnit> compilationUnits = librarySWFTarget.getCompilationUnits();
            for (IDefinition definition : getAllExternallyVisibleDefinitions(compilationUnits))
            {
                if (!(definition instanceof IClassDefinition))
                    continue;

                IClassDefinition classDefinition = (IClassDefinition)definition;
                
                IMetaTag iconFileMetaTag = classDefinition.getMetaTagByName(IMetaAttributeConstants.ATTRIBUTE_ICON_FILE);
                
                String iconFilePath = classDefinition.getIconFile();
                if (iconFilePath == null)
                    continue;

                String packageName = classDefinition.getPackageName();
                String key = packageName.replaceAll("\\.", "/") + "/" + iconFilePath;

                ICompilationUnit cu = project.getScope().getCompilationUnitForDefinition(classDefinition);
                IBinaryFileSpecification fileSpec = null;
                if (cu.getCompilationUnitType() == UnitType.SWC_UNIT)
                {
                    ISWC swc = project.getWorkspace().getSWCManager().get(new File(cu.getAbsoluteFilename()));
                    ISWCFileEntry swcFileEntry = swc.getFile(key);
                    fileSpec = new SWCFileSpecification(key, swcFileEntry);
                }
                else
                {
                    String sourcePath = classDefinition.getSourcePath();
                    iconFilePath = FilenameUtils.getFullPath(sourcePath) + iconFilePath;
                    fileSpec = project.getWorkspace().getLatestBinaryFileSpecification(iconFilePath);
                }

                FileEntryValue value = new FileEntryValue(fileSpec, iconFileMetaTag);
                iconFiles.put(key, value);
            }
        }
        catch (InterruptedException e)
        {
        }
        
        return iconFiles;
    }

    @Override
    protected ITargetReport computeTargetReport() throws InterruptedException
    {
        assert librarySWFTarget != null : "Must call build before getting the target report!!";
        return librarySWFTarget.getTargetReport();
    }
    
    private static class FileEntryValue
    {
        FileEntryValue(IBinaryFileSpecification fileSpec, ISourceLocation sourceLocation)
        {
            this.fileSpec = fileSpec;
            this.sourceLocation = sourceLocation;
        }
        
        private IBinaryFileSpecification fileSpec;
        private ISourceLocation sourceLocation;
        
        IBinaryFileSpecification getFileSpec()
        {
            return fileSpec;
        }
        
        ISourceLocation getSourceLocation()
        {
            return sourceLocation;
        }
    }
}
