/// <summary>
        /// Creates a file mapping between a set of source patterns and a target pattern. All patterns should have a matching order and number of wildcards.
        /// </summary>
        /// <param name="Files">Files to use for the mapping</param>
        /// <param name="SourcePatterns">List of source patterns</param>
        /// <param name="TargetPattern">Matching output pattern</param>
        /// <param name="Filter">Filter to apply to source files</param>
        /// <param name="TargetFileToSourceFile">Dictionary to receive a mapping from target file to source file. An exception is thrown if multiple source files map to one target file, or a source file is also used as a target file.</param>
        public static Dictionary <FileReference, FileReference> CreateMapping(HashSet <FileReference> Files, ref FilePattern SourcePattern, ref FilePattern TargetPattern)
        {
            // If the source pattern ends in a directory separator, or a set of input files are specified and it doesn't contain wildcards, treat it as a full directory match
            if (SourcePattern.EndsWithDirectorySeparator())
            {
                SourcePattern = new FilePattern(SourcePattern.BaseDirectory, String.Join("", SourcePattern.Tokens) + "...");
            }
            else if (Files != null)
            {
                SourcePattern = SourcePattern.AsDirectoryPattern();
            }

            // If we have multiple potential source files, but no wildcards in the output pattern, assume it's a directory and append the pattern from the source.
            if (SourcePattern.ContainsWildcards() && !TargetPattern.ContainsWildcards())
            {
                StringBuilder NewPattern = new StringBuilder();
                foreach (string Token in TargetPattern.Tokens)
                {
                    NewPattern.Append(Token);
                }
                if (NewPattern.Length > 0 && NewPattern[NewPattern.Length - 1] != Path.DirectorySeparatorChar)
                {
                    NewPattern.Append(Path.DirectorySeparatorChar);
                }
                foreach (string Token in SourcePattern.Tokens)
                {
                    NewPattern.Append(Token);
                }
                TargetPattern = new FilePattern(TargetPattern.BaseDirectory, NewPattern.ToString());
            }

            // If the target pattern ends with a directory separator, treat it as a full directory match if it has wildcards, or a copy of the source pattern if not
            if (TargetPattern.EndsWithDirectorySeparator())
            {
                TargetPattern = new FilePattern(TargetPattern.BaseDirectory, String.Join("", TargetPattern.Tokens) + "...");
            }

            // Handle the case where source and target pattern are both individual files
            Dictionary <FileReference, FileReference> TargetFileToSourceFile = new Dictionary <FileReference, FileReference>();

            if (SourcePattern.ContainsWildcards() || TargetPattern.ContainsWildcards())
            {
                // Check the two patterns are compatible
                if (!SourcePattern.IsCompatibleWith(TargetPattern))
                {
                    throw new FilePatternException("File patterns '{0}' and '{1}' do not have matching wildcards", SourcePattern, TargetPattern);
                }

                // Create a filter to match the source files
                FileFilter Filter = new FileFilter(FileFilterType.Exclude);
                Filter.Include(String.Join("", SourcePattern.Tokens));

                // Apply it to the source directory
                List <FileReference> SourceFiles;
                if (Files == null)
                {
                    SourceFiles = Filter.ApplyToDirectory(SourcePattern.BaseDirectory, true);
                }
                else
                {
                    SourceFiles = CheckInputFiles(Files, SourcePattern.BaseDirectory);
                }

                // Map them onto output files
                FileReference[] TargetFiles = new FileReference[SourceFiles.Count];

                // Get the source and target regexes
                string SourceRegex = SourcePattern.GetRegexPattern();
                string TargetRegex = TargetPattern.GetRegexReplacementPattern();
                for (int Idx = 0; Idx < SourceFiles.Count; Idx++)
                {
                    string SourceRelativePath = SourceFiles[Idx].MakeRelativeTo(SourcePattern.BaseDirectory);
                    string TargetRelativePath = Regex.Replace(SourceRelativePath, SourceRegex, TargetRegex);
                    TargetFiles[Idx] = FileReference.Combine(TargetPattern.BaseDirectory, TargetRelativePath);
                }

                // Add them to the output map
                for (int Idx = 0; Idx < TargetFiles.Length; Idx++)
                {
                    FileReference ExistingSourceFile;
                    if (TargetFileToSourceFile.TryGetValue(TargetFiles[Idx], out ExistingSourceFile) && ExistingSourceFile != SourceFiles[Idx])
                    {
                        throw new FilePatternException("Output file '{0}' is mapped from '{1}' and '{2}'", TargetFiles[Idx], ExistingSourceFile, SourceFiles[Idx]);
                    }
                    TargetFileToSourceFile[TargetFiles[Idx]] = SourceFiles[Idx];
                }
            }
            else
            {
                // Just copy a single file
                FileReference SourceFile = SourcePattern.GetSingleFile();
                if (FileReference.Exists(SourceFile))
                {
                    FileReference TargetFile = TargetPattern.GetSingleFile();
                    TargetFileToSourceFile[TargetFile] = SourceFile;
                }
                else
                {
                    throw new FilePatternException("Source file '{0}' does not exist", SourceFile);
                }
            }

            // Check that no source file is also destination file
            foreach (FileReference SourceFile in TargetFileToSourceFile.Values)
            {
                if (TargetFileToSourceFile.ContainsKey(SourceFile))
                {
                    throw new FilePatternException("'{0}' is listed as a source and target file", SourceFile);
                }
            }

            // Return the map
            return(TargetFileToSourceFile);
        }