// Copyright (c) Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Flax.Build.Graph;
using Flax.Build.NativeCpp;

namespace Flax.Build
{
    partial class Configuration
    {
        /// <summary>
        /// Specifies the initial memory size (in MB) to use by Web app.
        /// </summary>
        [CommandLine("webInitialMemory", "<size_mb>", "Specifies the initial memory size (in MB) to use by Web app.")]
        public static int WebInitialMemory = 32;
    }
}

namespace Flax.Build.Platforms
{
    /// <summary>
    /// The build toolchain for Web with Emscripten.
    /// </summary>
    /// <seealso cref="Toolchain" />
    public sealed class WebToolchain : Toolchain
    {
        private string _sysrootPath;
        private string _compilerPath;
        private Version _compilerVersion;

        /// <summary>
        /// Initializes a new instance of the <see cref="WebToolchain"/> class.
        /// </summary>
        /// <param name="platform">The platform.</param>
        /// <param name="architecture">The target architecture.</param>
        public WebToolchain(WebPlatform platform, TargetArchitecture architecture)
        : base(platform, architecture)
        {
            var sdkPath = EmscriptenSdk.Instance.EmscriptenPath;

            // Setup tools
            _compilerPath = Path.Combine(sdkPath, "emscripten", "emcc");
            var clangPath = Path.Combine(sdkPath, "bin", "clang++");
            if (Platform.BuildTargetPlatform == TargetPlatform.Windows)
            {
                _compilerPath += ".bat";
                clangPath += ".exe";
            }

            // Determinate compiler version
            _compilerVersion = UnixToolchain.GetClangVersion(platform.Target, clangPath);

            // Setup system paths
            SystemIncludePaths.Add(Path.Combine(sdkPath, "lib", "clang", _compilerVersion.Major.ToString(), "include"));
            SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "include"));
            SystemIncludePaths.Add(Path.Combine(sdkPath, "emscripten", "system", "lib"));
            _sysrootPath = Path.Combine(sdkPath, "emscripten/cache/sysroot/include");
        }

        public static string GetLibName(string path)
        {
            var libName = Path.GetFileNameWithoutExtension(path);
            if (libName.StartsWith("lib"))
                libName = libName.Substring(3);
            return libName;
        }

        /// <inheritdoc />
        public override string DllExport => "__attribute__((__visibility__(\\\"default\\\")))";

        /// <inheritdoc />
        public override string DllImport => "";

        /// <inheritdoc />
        public override TargetCompiler Compiler => TargetCompiler.Clang;

        /// <inheritdoc />
        public override string NativeCompilerPath => _compilerPath;

        /// <inheritdoc />
        public override void LogInfo()
        {
            Log.Info("Clang version: " + _compilerVersion);
        }

        /// <inheritdoc />
        public override void SetupEnvironment(BuildOptions options)
        {
            base.SetupEnvironment(options);

            options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_WEB");
            options.CompileEnv.PreprocessorDefinitions.Add("PLATFORM_UNIX");
            options.CompileEnv.PreprocessorDefinitions.Add("__EMSCRIPTEN__");
            options.CompileEnv.EnableExceptions = false;
            options.CompileEnv.CpuArchitecture = CpuArchitecture.None; // TODO: try SIMD support in Emscripten
        }

        private void AddSharedArgs(List<string> args, BuildOptions options, bool debugInformation, bool optimization)
        {
            //args.Add("-pthread");

            if (debugInformation)
                args.Add("-g2");
            else
                args.Add("-g0");

            if (options.CompileEnv.FavorSizeOrSpeed == FavorSizeOrSpeed.SmallCode)
                args.Add("-Os");
            if (options.CompileEnv.FavorSizeOrSpeed == FavorSizeOrSpeed.FastCode)
                args.Add("-O3");
            else if (optimization && options.Configuration == TargetConfiguration.Release)
                args.Add("-O3");
            else if (optimization)
                args.Add("-O2");
            else
                args.Add("-O0");

            if (options.CompileEnv.RuntimeTypeInfo)
                args.Add("-frtti");
            else
                args.Add("-fno-rtti");

            if (options.CompileEnv.TreatWarningsAsErrors)
                args.Add("-Wall -Werror");

            if (options.CompileEnv.EnableExceptions)
                args.Add("-fexceptions");
            else
                args.Add("-fno-exceptions");

            if (options.LinkEnv.LinkTimeCodeGeneration)
                args.Add("-flto");

            //if (options.LinkEnv.Output == LinkerOutput.SharedLibrary)
                args.Add("-fPIC");

            var sanitizers = options.CompileEnv.Sanitizers;
            if (sanitizers.HasFlag(Sanitizer.Address))
                args.Add("-fsanitize=address");
            if (sanitizers.HasFlag(Sanitizer.Undefined))
                args.Add("-fsanitize=undefined");
            if (sanitizers == Sanitizer.None)
                args.Add("-fsanitize=null -fsanitize-minimal-runtime"); // Minimal Runtime
        }

        /// <inheritdoc />
        public override CompileOutput CompileCppFiles(TaskGraph graph, BuildOptions options, List<string> sourceFiles, string outputPath)
        {
            var output = new CompileOutput();

            // Setup arguments shared by all source files
            var commonArgs = new List<string>();
            commonArgs.AddRange(options.CompileEnv.CustomArgs);
            {
                commonArgs.Add("-c");

                AddSharedArgs(commonArgs, options, options.CompileEnv.DebugInformation, options.CompileEnv.Optimization);

                // Hack to pull SDL3 port via emcc
                //if (options.CompileEnv.PreprocessorDefinitions.Contains("PLATFORM_SDL"))
                //    commonArgs.Add("--use-port=sdl3");
            }

            // Add preprocessor definitions
            foreach (var definition in options.CompileEnv.PreprocessorDefinitions)
            {
                commonArgs.Add(string.Format("-D \"{0}\"", definition));
            }

            // Add include paths
            foreach (var includePath in options.CompileEnv.IncludePaths)
            {
                if (SystemIncludePaths.Contains(includePath)) // TODO: fix SystemIncludePaths so this chack can be removed
                    continue; // Skip system includes as those break compilation (need to fix sys root linking for emscripten)
                commonArgs.Add(string.Format("-I\"{0}\"", includePath.Replace('\\', '/')));
            }

            // Hack for sysroot includes
            commonArgs.Add(string.Format("-I\"{0}\"", _sysrootPath.Replace('\\', '/')));

            // Compile all C/C++ files
            var args = new List<string>();
            foreach (var sourceFile in sourceFiles)
            {
                var sourceFilename = Path.GetFileNameWithoutExtension(sourceFile);
                var task = graph.Add<CompileCppTask>();

                // Use shared arguments
                args.Clear();
                args.AddRange(commonArgs);

                // Language for the file
                args.Add("-x");
                if (sourceFile.EndsWith(".c", StringComparison.OrdinalIgnoreCase))
                    args.Add("c");
                else
                {
                    args.Add("c++");

                    // C++ version
                    switch (options.CompileEnv.CppVersion)
                    {
                    case CppVersion.Cpp14:
                        args.Add("-std=c++14");
                        break;
                    case CppVersion.Cpp17:
                    case CppVersion.Latest:
                        args.Add("-std=c++17");
                        break;
                    case CppVersion.Cpp20:
                        args.Add("-std=c++20");
                        break;
                    }
                }

                // Object File Name
                var objFile = Path.Combine(outputPath, sourceFilename + ".o");
                args.Add(string.Format("-o \"{0}\"", objFile.Replace('\\', '/')));
                output.ObjectFiles.Add(objFile);
                task.ProducedFiles.Add(objFile);

                // Source File Name
                args.Add("\"" + sourceFile.Replace('\\', '/') + "\"");

                // Request included files to exist
                var includes = IncludesCache.FindAllIncludedFiles(sourceFile);
                task.PrerequisiteFiles.AddRange(includes);

                // Compile
                task.WorkingDirectory = options.WorkingDirectory;
                task.CommandPath = _compilerPath;
                task.CommandArguments = string.Join(" ", args);
                task.PrerequisiteFiles.Add(sourceFile);
                task.InfoMessage = Path.GetFileName(sourceFile);
                task.Cost = task.PrerequisiteFiles.Count; // TODO: include source file size estimation to improve tasks sorting
            }

            return output;
        }

        public override bool CompileCSharp(ref CSharpOptions options)
        {
            switch (options.Action)
            {
            /*case CSharpOptions.ActionTypes.GetOutputFiles:
            {
                foreach (var inputFile in options.InputFiles)
                {
                    string assemblyPath;
                    if (Configuration.AOTMode == DotNetAOTModes.MonoAOTDynamic)
                        assemblyPath = inputFile + Platform.SharedLibraryFileExtension;
                    else
                        assemblyPath = Path.Combine(Path.GetDirectoryName(inputFile), Platform.StaticLibraryFilePrefix + Path.GetFileName(inputFile) + Platform.StaticLibraryFileExtension);

                    if (Path.GetFileNameWithoutExtension(inputFile) == "System.Private.CoreLib")
                    {
                        // Use pre-compiled binaries
                        DotNetSdk.Instance.GetHostRuntime(TargetPlatform.Web, TargetArchitecture.x86, out var hostRuntime);
                        assemblyPath = Path.Combine(hostRuntime.Path, "libmonosgen-2.0.a");
                    }
                    options.OutputFiles.Add(assemblyPath);
                }
                return false;
            }*/
            case CSharpOptions.ActionTypes.GetPlatformTools:
            {
                string arch = Platform.BuildTargetArchitecture switch
                {
                    TargetArchitecture.x64 => "x64",
                    TargetArchitecture.ARM64 => "arm64",
                    _ => throw new PlatformNotSupportedException(Platform.BuildTargetArchitecture.ToString()),
                };
                string os;
                switch (Platform.BuildPlatform.Target)
                {
                case TargetPlatform.Windows:
                    os = "win";
                    break;
                default:
                    throw new PlatformNotSupportedException(Platform.BuildPlatform.Target.ToString());
                }
                
                options.PlatformToolsPath = Path.Combine(DotNetSdk.SelectVersionFolder(Path.Combine(DotNetSdk.Instance.RootPath, $"packs/Microsoft.NETCore.App.Runtime.AOT.{os}-{arch}.Cross.browser-wasm")), "tools");
                return false;
            }
            case CSharpOptions.ActionTypes.MonoLink:
            {
                // Setup arguments
                var args = new List<string>();
                //args.AddRange(options.LinkEnv.CustomArgs);
                {
                    args.Add(string.Format("-o \"{0}\"", options.OutputFiles[0].Replace('\\', '/')));

                    //AddSharedArgs(args, options, false, false);

                    // Setup memory
                    var initialMemory = Configuration.WebInitialMemory;
                    //if (options.CompileEnv.Sanitizers.HasFlag(Sanitizer.Address))
                    //    initialMemory = Math.Max(initialMemory, 64); // Address Sanitizer needs more memory
                    /*args.Add($"-sINITIAL_MEMORY={initialMemory}MB");
                    args.Add("-sSTACK_SIZE=4MB");
                    args.Add("-sALLOW_MEMORY_GROWTH=1");

                    // Setup file access (Game Cooker packs files with file_packager tool)
                    args.Add("-sFORCE_FILESYSTEM");*/
                    //args.Add("-sLZ4");

                    // https://emscripten.org/docs/compiling/Dynamic-Linking.html#dynamic-linking
                    // TODO: use -sMAIN_MODULE=2 and -sSIDE_MODULE=2 to strip unused code (mark public APIs with EMSCRIPTEN_KEEPALIVE)
                    /*if (options.LinkEnv.Output == LinkerOutput.Executable)
                    {
                        args.Add("-sMAIN_MODULE");
                        args.Add("-sEXPORT_ALL");
                    }
                    else*/
                    {
                        args.Add("-sSIDE_MODULE");
                    }
                }

                args.Add("-Wl,--allow-multiple-definition"); // Multiple pthread-related definitions in dotnet runtime

                args.Add("-Wl,--start-group");

                // Input libraries
                var libraryPaths = new HashSet<string>();
                var dynamicLibExt = Platform.SharedLibraryFileExtension;
                var executableExt = Platform.ExecutableFileExtension;
                foreach (var library in options.InputFiles)
                {
                    /*var dir = Path.GetDirectoryName(library);
                    var ext = Path.GetExtension(library);
                    if (library.StartsWith("--use-port="))
                    {
                        // Ports (https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-ports)
                        args.Add(library);
                    }
                    else if (string.IsNullOrEmpty(dir))
                    {
                        args.Add(string.Format("\"-l{0}\"", library));
                    }
                    else if (ext == executableExt)
                    {
                        // Skip executable
                    }
                    else if (ext == dynamicLibExt)
                    {
                        // Link against dynamic library
                        //task.PrerequisiteFiles.Add(library);
                        libraryPaths.Add(dir);
                        args.Add(string.Format("\"-l{0}\"", GetLibName(library)));
                    }
                    else
                    {
                        //task.PrerequisiteFiles.Add(library);
                        args.Add(string.Format("\"-l{0}\"", GetLibName(library)));
                        libraryPaths.Add(dir); // FIXME
                    }*/
                    args.Add(library.Replace(Path.DirectorySeparatorChar, '/') + Platform.StaticLibraryFileExtension);
                }

                // Input files (link static libraries last)
                /*task.PrerequisiteFiles.AddRange(options.LinkEnv.InputFiles);
                foreach (var file in options.LinkEnv.InputFiles.Where(x => !x.EndsWith(".a")).Concat(options.LinkEnv.InputFiles.Where(x => x.EndsWith(".a"))))
                {
                    args.Add(string.Format("\"{0}\"", file.Replace('\\', '/')));
                }

                // Additional lib paths
                libraryPaths.AddRange(options.LinkEnv.LibraryPaths);
                foreach (var path in libraryPaths)
                {
                    args.Add(string.Format("-L\"{0}\"", path.Replace('\\', '/')));
                }*/

                args.Add("-Wl,--end-group");

                // Use a response file (it can contain any commands that you would specify on the command line)
                bool useResponseFile = true;
                string responseFile = null;
                if (useResponseFile)
                {
                    responseFile = Path.Combine(options.AssembliesPath, Path.GetFileName(options.OutputFiles[0]) + ".response");
                    //task.PrerequisiteFiles.Add(responseFile);
                    Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args));
                    args.Clear();
                    args.Add(string.Format("@\"{0}\"", responseFile));
                }

                //task.WorkingDirectory = options.WorkingDirectory;
                //task.CommandPath = _compilerPath;
                //task.CommandArguments = string.Join(" ", args);

                int result = Utilities.Run(NativeCompilerPath, $"{string.Join(" ", args)}", null, Path.GetDirectoryName(options.OutputFiles[0]), Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput);
                if (result != 0)
                    return true;

                return false;
            }
            case CSharpOptions.ActionTypes.MonoCompile:
            {
                string binaryExtension = Platform.BuildPlatform.Target == TargetPlatform.Windows ? ".exe" : "";
                var aotCompilerPath = Path.Combine(options.PlatformToolsPath, "mono-aot-cross") + binaryExtension;
                //var clangPath = Path.Combine(ToolchainPath, "usr/bin/clang");

                var inputFile = options.InputFiles[0];
                var inputFileAsm = inputFile + ".s";
                var inputFileObj = inputFile + ".o";
                var outputFileDylib = options.OutputFiles[0];
                var inputFileFolder = Path.GetDirectoryName(inputFile);

                /*if (Path.GetFileNameWithoutExtension(inputFile) == "System.Private.CoreLib")
                {
                    // Pre-compiled, skip
                    return false;
                }*/

                string outputFile;
                {
                    if (Configuration.AOTMode == DotNetAOTModes.MonoAOTDynamic)
                        outputFile = inputFile + Platform.SharedLibraryFileExtension;
                    else
                        outputFile = Path.Combine(Path.GetDirectoryName(inputFile), Platform.StaticLibraryFilePrefix + Path.GetFileName(inputFile) + Platform.StaticLibraryFileExtension);
                }

                // Setup options
                bool debugSymbols = options.EnableDebugSymbols;
                bool useLLVM = true;
                //if (useLLVM)
                    debugSymbols = false;
                var llvmPath = "\"C:\\Program Files\\LLVM\\bin\"";//Path.Combine(EmscriptenSdk.Instance.EmscriptenPath, "bin");

                var monoAotMode = "full";
                if (useLLVM)
                    monoAotMode = "llvmonly";
                //if ("mscorlib.dll" == "")
                //    monoAotMode = "interp";

                var monoDebugMode = debugSymbols ? "soft-debug" : "nodebug";
                var aotCompilerArgs = $"{(useLLVM ? "--llvm" : "")} --aot={monoAotMode},asmonly,verbose,stats,print-skipped,{monoDebugMode}{(useLLVM ? $",llvm-path={llvmPath},llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32";

                //aotCompilerArgs += " --verbose";
                //aotCompilerArgs += $" --aot-path=\"{inputFileFolder}\"";
                //var aotCompilerArgs = $"{(useLLVM ? " --llvm" : "")} --aot=static,llvmonly,verbose,stats,print-skipped,llvm-path={llvmPath},{monoDebugMode}{(useLLVM ? ",llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32";
                //var aotCompilerArgs = $"{(useLLVM ? " --llvm" : "")} --aot=static,llvmonly,verbose,stats,print-skipped,{monoDebugMode}{(useLLVM ? ",llvm-outfile=" + Path.GetFileName(outputFile) : "")} -O=float32";
                //if (debugSymbols || options.EnableToolDebug)
                //    aotCompilerArgs = "--debug " + aotCompilerArgs;

                var envVars = new Dictionary<string, string>();
                envVars["MONO_PATH"] = options.ClassLibraryPath.Replace('/', Path.DirectorySeparatorChar) + Path.PathSeparator + options.AssembliesPath.Replace('/', Path.DirectorySeparatorChar);
                Log.Info("MONO_PATH: " + envVars["MONO_PATH"]);
                if (options.EnableToolDebug)
                {
                    envVars["MONO_LOG_LEVEL"] = "debug";
                }

                //envVars["MONO_DEBUG"] = "gen-seq-points";
                

                // Run cross-compiler compiler (outputs assembly code)
                int result = Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{inputFile}\"", null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars);
                if (result != 0)
                    return true;

                // Get build args for iOS
                /*var clangArgs = new List<string>();
                AddArgsCommon(null, clangArgs);
                var clangArgsText = string.Join(" ", clangArgs);

                // Build object file
                result = Utilities.Run(clangPath, $"\"{inputFileAsm}\" -c -o \"{inputFileObj}\" " + clangArgsText, null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars);
                if (result != 0)
                    return true;

                // Build dylib file
                result = Utilities.Run(clangPath, $"\"{inputFileObj}\" -dynamiclib -fPIC -o \"{outputFileDylib}\" " + clangArgsText, null, inputFileFolder, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars);
                if (result != 0)
                    return true;

                // Clean intermediate results
                File.Delete(inputFileAsm);
                File.Delete(inputFileObj);

                // Fix rpath id
                result = Utilities.Run("install_name_tool", $"-id \"@rpath/{Path.GetFileName(outputFileDylib)}\" \"{outputFileDylib}\"", null, inputFileFolder, Utilities.RunOptions.ConsoleLogOutput, envVars);
                if (result != 0)
                    return true;*/

                return false;
            }
            }
            return base.CompileCSharp(ref options);
        }
#if false
        /// <inheritdoc />
        public override bool CompileCSharp(ref CSharpOptions options)
        {
            switch (options.Action)
            {
            case CSharpOptions.ActionTypes.MonoCompile:
            {
                var aotCompilerPath = Path.Combine(options.PlatformToolsPath, "mono-aot-cross.exe");

                // Setup options
                var monoAotMode = "full";
                var monoDebugMode = options.EnableDebugSymbols ? "soft-debug" : "nodebug";
                var aotCompilerArgs = $"--aot={monoAotMode},verbose,stats,print-skipped,{monoDebugMode} -O=all";
                if (options.EnableDebugSymbols || options.EnableToolDebug)
                    aotCompilerArgs = "--debug " + aotCompilerArgs;
                var envVars = new Dictionary<string, string>();
                envVars["MONO_PATH"] = options.AssembliesPath + ";" + options.ClassLibraryPath;
                if (options.EnableToolDebug)
                {
                    envVars["MONO_LOG_LEVEL"] = "debug";
                }

                // Run cross-compiler compiler
                int result = Utilities.Run(aotCompilerPath, $"{aotCompilerArgs} \"{options.InputFiles[0]}\"", null, ""/*options.PlatformToolsPath*/, Utilities.RunOptions.AppMustExist | Utilities.RunOptions.ConsoleLogOutput, envVars);
                return result != 0;
            }
            }
            return base.CompileCSharp(ref options);
        }
#endif

        private Task CreateBinary(TaskGraph graph, BuildOptions options, string outputFilePath)
        {
            var task = graph.Add<LinkTask>();

            // Setup arguments
            var args = new List<string>();
            args.AddRange(options.LinkEnv.CustomArgs);
            {
                args.Add(string.Format("-o \"{0}\"", outputFilePath.Replace('\\', '/')));

                AddSharedArgs(args, options, options.LinkEnv.DebugInformation, options.LinkEnv.Optimization);

                // Setup memory
                var initialMemory = Configuration.WebInitialMemory;
                if (options.CompileEnv.Sanitizers.HasFlag(Sanitizer.Address))
                    initialMemory = Math.Max(initialMemory, 64); // Address Sanitizer needs more memory
                args.Add($"-sINITIAL_MEMORY={initialMemory}MB");
                args.Add("-sSTACK_SIZE=4MB");
                args.Add("-sALLOW_MEMORY_GROWTH=1");

                // Setup file access (Game Cooker packs files with file_packager tool)
                args.Add("-sFORCE_FILESYSTEM");
                args.Add("-sLZ4");

                // https://emscripten.org/docs/compiling/Dynamic-Linking.html#dynamic-linking
                // TODO: use -sMAIN_MODULE=2 and -sSIDE_MODULE=2 to strip unused code (mark public APIs with EMSCRIPTEN_KEEPALIVE)
                if (options.LinkEnv.Output == LinkerOutput.Executable)
                {
                    args.Add("-sMAIN_MODULE");
                    args.Add("-sEXPORT_ALL");
                }
                else
                {
                    args.Add("-sSIDE_MODULE");
                }
            }

            args.Add("-Wl,--allow-multiple-definition"); // Multiple pthread-related definitions in dotnet runtime
 
            args.Add("-Wl,--start-group");

            // Input libraries
            var libraryPaths = new HashSet<string>();
            var dynamicLibExt = Platform.SharedLibraryFileExtension;
            var executableExt = Platform.ExecutableFileExtension;
            foreach (var library in options.LinkEnv.InputLibraries.Concat(options.Libraries))
            {
                var dir = Path.GetDirectoryName(library);
                var ext = Path.GetExtension(library);
                if (library.StartsWith("--use-port="))
                {
                    // Ports (https://emscripten.org/docs/compiling/Building-Projects.html#emscripten-ports)
                    args.Add(library);
                }
                else if (string.IsNullOrEmpty(dir))
                {
                    args.Add(string.Format("\"-l{0}\"", library));
                }
                else if (ext == executableExt)
                {
                    // Skip executable
                }
                else if (ext == dynamicLibExt)
                {
                    // Link against dynamic library
                    task.PrerequisiteFiles.Add(library);
                    libraryPaths.Add(dir);
                    args.Add(string.Format("\"-l{0}\"", GetLibName(library)));
                }
                else
                {
                    task.PrerequisiteFiles.Add(library);
                    args.Add(string.Format("\"-l{0}\"", GetLibName(library)));
                    libraryPaths.Add(dir); // FIXME
                }
            }

            // Input files (link static libraries last)
            task.PrerequisiteFiles.AddRange(options.LinkEnv.InputFiles);
            foreach (var file in options.LinkEnv.InputFiles.Where(x => !x.EndsWith(".a")).Concat(options.LinkEnv.InputFiles.Where(x => x.EndsWith(".a"))))
            {
                args.Add(string.Format("\"{0}\"", file.Replace('\\', '/')));
            }

            // Additional lib paths
            libraryPaths.AddRange(options.LinkEnv.LibraryPaths);
            foreach (var path in libraryPaths)
            {
                args.Add(string.Format("-L\"{0}\"", path.Replace('\\', '/')));
            }

            args.Add("-Wl,--end-group");

            // Use a response file (it can contain any commands that you would specify on the command line)
            bool useResponseFile = true;
            string responseFile = null;
            if (useResponseFile)
            {
                responseFile = Path.Combine(options.IntermediateFolder, Path.GetFileName(outputFilePath) + ".response");
                task.PrerequisiteFiles.Add(responseFile);
                Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args));
            }

            // Link
            task.WorkingDirectory = options.WorkingDirectory;
            task.CommandPath = _compilerPath;
            task.CommandArguments = useResponseFile ? string.Format("@\"{0}\"", responseFile) : string.Join(" ", args);
            task.InfoMessage = "Linking " + outputFilePath;
            task.Cost = task.PrerequisiteFiles.Count;
            task.ProducedFiles.Add(outputFilePath);

            return task;
        }

        /// <inheritdoc />
        public override void LinkFiles(TaskGraph graph, BuildOptions options, string outputFilePath)
        {
            outputFilePath = Utilities.NormalizePath(outputFilePath);

            Task linkTask;
            switch (options.LinkEnv.Output)
            {
            case LinkerOutput.Executable:
            case LinkerOutput.SharedLibrary:
                linkTask = CreateBinary(graph, options, outputFilePath);
                break;
            case LinkerOutput.StaticLibrary:
            case LinkerOutput.ImportLibrary:
            default:
                throw new ArgumentOutOfRangeException();
            }
        }
    }
}
