// Copyright (c) 2015 Daniel Grunwald
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection.PortableExecutable;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml.Linq;
using System.Xml.XPath;

using CliWrap;
using CliWrap.Buffered;

using ICSharpCode.Decompiler.CSharp;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.Decompiler.CSharp.Transforms;
using ICSharpCode.Decompiler.Disassembler;
using ICSharpCode.Decompiler.Metadata;
using ICSharpCode.Decompiler.TypeSystem;
using ICSharpCode.ILSpyX.PdbProvider;

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;

using NUnit.Framework;

namespace ICSharpCode.Decompiler.Tests.Helpers
{
	[Flags]
	public enum CompilerOptions
	{
		None,
		Optimize = 0x1,
		UseDebug = 0x2,
		Force32Bit = 0x4,
		Library = 0x8,
		UseRoslyn1_3_2 = 0x10,
		UseMcs2_6_4 = 0x20,
		ReferenceVisualBasic = 0x40,
		TargetNet40 = 0x80,
		GeneratePdb = 0x100,
		Preview = 0x200,
		UseRoslyn2_10_0 = 0x400,
		UseRoslyn3_11_0 = 0x800,
		UseRoslynLatest = 0x1000,
		UseMcs5_23 = 0x2000,
		UseTestRunner = 0x4000,
		NullableEnable = 0x8000,
		ReferenceUnsafe = 0x10000,
		UseMcsMask = UseMcs2_6_4 | UseMcs5_23,
		UseRoslynMask = UseRoslyn1_3_2 | UseRoslyn2_10_0 | UseRoslyn3_11_0 | UseRoslynLatest
	}

	[Flags]
	public enum AssemblerOptions
	{
		None,
		UseDebug = 0x1,
		Force32Bit = 0x2,
		Library = 0x4,
		/// Testing our own disassembler, or working around a bug in ildasm.
		UseOwnDisassembler = 0x8,
		/// Work around bug in .NET 5 ilasm (https://github.com/dotnet/runtime/issues/32400)
		UseLegacyAssembler = 0x10,
		/// UseSortByNameFilter, implies UseOwnDisassembler
		SortedOutput = 0x20,
	}

	public static partial class Tester
	{
		public static readonly string TesterPath;
		public static readonly string TestCasePath;

		static readonly string testRunnerBasePath;
		static readonly string packagesPropsFile;
		static readonly string roslynLatestVersion;
		static readonly RoslynToolset roslynToolset;
		static readonly VsWhereToolset vswhereToolset;

		static Tester()
		{
			TesterPath = Path.GetDirectoryName(typeof(Tester).Assembly.Location);
			TestCasePath = Path.Combine(TesterPath, "../../../../TestCases");
#if DEBUG
			testRunnerBasePath = Path.Combine(TesterPath, "../../../../../ICSharpCode.Decompiler.TestRunner/bin/Debug/net8.0");
#else
			testRunnerBasePath = Path.Combine(TesterPath, "../../../../../ICSharpCode.Decompiler.TestRunner/bin/Release/net8.0");
#endif
			// To parse: <Project><ItemGroup><PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.8.0-3.final" />
			packagesPropsFile = Path.Combine(TesterPath, "../../../../../Directory.Packages.props");
			roslynLatestVersion = ((IEnumerable<object>)(XDocument
				.Load(packagesPropsFile)
				.XPathEvaluate("//Project//ItemGroup//PackageVersion[@Include='Microsoft.CodeAnalysis.CSharp']/@Version")))
				.OfType<XAttribute>()
				.Single()
				.Value;

			roslynToolset = new RoslynToolset();
			vswhereToolset = new VsWhereToolset();
		}

		internal static async Task Initialize()
		{
			await roslynToolset.Fetch("1.3.2", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false);
			await roslynToolset.Fetch("2.10.0", "Microsoft.Net.Compilers", "tools").ConfigureAwait(false);
			await roslynToolset.Fetch("3.11.0").ConfigureAwait(false);
			await roslynToolset.Fetch(roslynLatestVersion).ConfigureAwait(false);

			await vswhereToolset.Fetch().ConfigureAwait(false);

#if DEBUG
			await BuildTestRunner("win-x86", "Debug").ConfigureAwait(false);
			await BuildTestRunner("win-x64", "Debug").ConfigureAwait(false);
#else
			await BuildTestRunner("win-x86", "Release").ConfigureAwait(false);
			await BuildTestRunner("win-x64", "Release").ConfigureAwait(false);
#endif
		}

		static async Task BuildTestRunner(string runtime, string config)
		{
			await Cli.Wrap("dotnet.exe")
				.WithArguments(new[] { "build", Path.Combine(TesterPath, "../../../../../ICSharpCode.Decompiler.TestRunner/ICSharpCode.Decompiler.TestRunner.csproj"), "-r", runtime, "-c", config, "--self-contained" })
				.ExecuteAsync();
		}

		public static async Task<string> AssembleIL(string sourceFileName, AssemblerOptions options = AssemblerOptions.UseDebug)
		{
			string ilasmPath;
			if (options.HasFlag(AssemblerOptions.UseLegacyAssembler))
			{
				ilasmPath = Path.Combine(Environment.GetEnvironmentVariable("windir"), @"Microsoft.NET\Framework\v4.0.30319\ilasm.exe");
			}
			else
			{
				ilasmPath = Path.Combine(
					Path.GetDirectoryName(typeof(Tester).Assembly.Location),
					"ilasm.exe");
			}
			string outputFile = Path.Combine(Path.GetDirectoryName(sourceFileName), Path.GetFileNameWithoutExtension(sourceFileName));
			string otherOptions = " ";
			if (options.HasFlag(AssemblerOptions.Force32Bit))
			{
				outputFile += ".32";
				otherOptions += "/32BitPreferred ";
			}
			if (options.HasFlag(AssemblerOptions.Library))
			{
				outputFile += ".dll";
				otherOptions += "/dll ";
			}
			else
			{
				outputFile += ".exe";
				otherOptions += "/exe ";
			}


			if (options.HasFlag(AssemblerOptions.UseDebug))
			{
				otherOptions += "/debug ";
			}

			var command = Cli.Wrap(ilasmPath)
				.WithArguments($"/quiet {otherOptions}/output=\"{outputFile}\" \"{sourceFileName}\"")
				.WithValidation(CommandResultValidation.None);

			var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);

			if (!string.IsNullOrWhiteSpace(result.StandardOutput))
			{
				Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
			}
			if (!string.IsNullOrWhiteSpace(result.StandardError))
			{
				Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
			}
			Assert.AreEqual(0, result.ExitCode, "ilasm failed");

			return outputFile;
		}

		public static async Task<string> Disassemble(string sourceFileName, string outputFile, AssemblerOptions asmOptions)
		{
			if (asmOptions.HasFlag(AssemblerOptions.UseOwnDisassembler) || asmOptions.HasFlag(AssemblerOptions.SortedOutput))
			{
				using (var peFileStream = new FileStream(sourceFileName, FileMode.Open, FileAccess.Read))
				using (var peFile = new PEFile(sourceFileName, peFileStream))
				using (var writer = new StringWriter())
				{
					var metadata = peFile.Metadata;
					var output = new PlainTextOutput(writer);
					ReflectionDisassembler rd = new ReflectionDisassembler(output, CancellationToken.None);
					if (asmOptions.HasFlag(AssemblerOptions.SortedOutput))
					{
						rd.EntityProcessor = new SortByNameProcessor();
					}
					rd.AssemblyResolver = new UniversalAssemblyResolver(sourceFileName, throwOnError: true, null);
					rd.DetectControlStructure = false;
					rd.WriteAssemblyReferences(metadata);
					if (metadata.IsAssembly)
						rd.WriteAssemblyHeader(peFile);
					output.WriteLine();
					rd.WriteModuleHeader(peFile, skipMVID: true);
					output.WriteLine();
					rd.WriteModuleContents(peFile);

					File.WriteAllText(outputFile, ReplacePrivImplDetails(writer.ToString()));
				}
				return outputFile;
			}

			string ildasmPath = Path.Combine(
				Path.GetDirectoryName(typeof(Tester).Assembly.Location),
				"ildasm.exe");

			var command = Cli.Wrap(ildasmPath)
				.WithArguments($"/utf8 /out=\"{outputFile}\" \"{sourceFileName}\"")
				.WithValidation(CommandResultValidation.None);

			var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);

			if (!string.IsNullOrWhiteSpace(result.StandardOutput))
			{
				Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
			}
			if (!string.IsNullOrWhiteSpace(result.StandardError))
			{
				Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
			}
			Assert.AreEqual(0, result.ExitCode, "ildasm failed");

			// Unlike the .imagebase directive (which is a fixed value when compiling with /deterministic),
			// the image base comment still varies... ildasm putting a random number here?
			string il = File.ReadAllText(outputFile);
			il = Regex.Replace(il, @"^// Image base: 0x[0-9A-F]+\r?\n", "", RegexOptions.Multiline);
			// and while we're at it, also remove the MVID
			il = Regex.Replace(il, @"^// MVID: \{[0-9A-F-]+\}\r?\n", "", RegexOptions.Multiline);
			// and the ildasm version info (varies from system to system)
			il = Regex.Replace(il, @"^// +Microsoft .* Disassembler\. +Version.*\r?\n", "", RegexOptions.Multiline);
			// copyright header "All rights reserved" is dependent on system language
			il = Regex.Replace(il, @"^// +Copyright .* Microsoft.*\r?\n", "", RegexOptions.Multiline);
			// filename may contain full path
			il = Regex.Replace(il, @"^// WARNING: Created Win32 resource file.*\r?\n", "", RegexOptions.Multiline);
			il = ReplacePrivImplDetails(il);
			File.WriteAllText(outputFile, il);

			return outputFile;
		}

		private static string ReplacePrivImplDetails(string il)
		{
			return Regex.Replace(il, @"'<PrivateImplementationDetails>\{[0-9A-F-]+\}'", "'<PrivateImplementationDetails>'");
		}

		static readonly string coreRefAsmPath = new DotNetCorePathFinder(TargetFrameworkIdentifier.NET,
			new Version(8, 0), "Microsoft.NETCore.App")
				.GetReferenceAssemblyPath(".NETCoreApp,Version=v8.0");

		public static readonly string RefAsmPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
			@"Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2");

		static readonly string[] defaultReferences = new[] {
			"System.dll",
			"System.Core.dll",
			"System.Xml.dll",
			"Microsoft.CSharp.dll"
		};

		static readonly string[] coreDefaultReferences = new[]
			{
				"netstandard.dll",
				"mscorlib.dll",
				"System.dll",
				"System.Collections.dll",
				"System.Console.dll",
				"System.Core.dll",
				"System.Linq.dll",
				"System.Linq.Expressions.dll",
				"System.Linq.Queryable.dll",
				"System.IO.FileSystem.Watcher.dll",
				"System.Memory.dll",
				"System.Threading.dll",
				"System.Threading.Thread.dll",
				"System.Runtime.dll",
				"System.Runtime.InteropServices.dll",
				"System.Xml.dll",
				"System.Xml.ReaderWriter.dll",
				"System.ValueTuple.dll",
				"Microsoft.CSharp.dll",
				"Microsoft.VisualBasic.dll",
			};

		const string targetFrameworkAttributeSnippet = @"

[assembly: System.Runtime.Versioning.TargetFramework("".NETCoreApp,Version=v8.0"", FrameworkDisplayName = """")]

";

		static readonly Lazy<string> targetFrameworkAttributeSnippetFile = new Lazy<string>(GetTargetFrameworkAttributeSnippetFile);

		static string GetTargetFrameworkAttributeSnippetFile()
		{
			// Note: this leaks a temporary file, we're not attempting to delete it, because it is only one.
			var tempFile = Path.GetTempFileName();
			File.WriteAllText(tempFile, targetFrameworkAttributeSnippet);
			return tempFile;
		}

		public static List<string> GetPreprocessorSymbols(CompilerOptions flags)
		{
			var preprocessorSymbols = new List<string>();
			if (flags.HasFlag(CompilerOptions.UseDebug))
			{
				preprocessorSymbols.Add("DEBUG");
			}
			if (flags.HasFlag(CompilerOptions.Optimize))
			{
				preprocessorSymbols.Add("OPT");
			}
			if (flags.HasFlag(CompilerOptions.TargetNet40))
			{
				preprocessorSymbols.Add("NET40");
			}
			if ((flags & CompilerOptions.UseRoslynMask) != 0)
			{
				if (!flags.HasFlag(CompilerOptions.TargetNet40))
				{
					preprocessorSymbols.Add("NETCORE");
					preprocessorSymbols.Add("NET60");
					preprocessorSymbols.Add("NET70");
					preprocessorSymbols.Add("NET80");
				}
				preprocessorSymbols.Add("ROSLYN");
				preprocessorSymbols.Add("CS60");
				preprocessorSymbols.Add("VB11");
				preprocessorSymbols.Add("VB14");
				if (flags.HasFlag(CompilerOptions.UseRoslyn2_10_0)
					|| flags.HasFlag(CompilerOptions.UseRoslyn3_11_0)
					|| flags.HasFlag(CompilerOptions.UseRoslynLatest))
				{
					preprocessorSymbols.Add("ROSLYN2");
					preprocessorSymbols.Add("CS70");
					preprocessorSymbols.Add("CS71");
					preprocessorSymbols.Add("CS72");
					preprocessorSymbols.Add("CS73");
					preprocessorSymbols.Add("VB15");
				}
				if (flags.HasFlag(CompilerOptions.UseRoslyn3_11_0)
					|| flags.HasFlag(CompilerOptions.UseRoslynLatest))
				{
					preprocessorSymbols.Add("ROSLYN3");
					preprocessorSymbols.Add("CS80");
					preprocessorSymbols.Add("CS90");
					preprocessorSymbols.Add("VB16");
				}
				if (flags.HasFlag(CompilerOptions.UseRoslynLatest))
				{
					preprocessorSymbols.Add("ROSLYN4");
					preprocessorSymbols.Add("CS100");
					preprocessorSymbols.Add("CS110");
					preprocessorSymbols.Add("CS120");
				}
			}
			else if ((flags & CompilerOptions.UseMcsMask) != 0)
			{
				preprocessorSymbols.Add("MCS");
				if (flags.HasFlag(CompilerOptions.UseMcs2_6_4))
				{
					preprocessorSymbols.Add("MCS2");
				}
				if (flags.HasFlag(CompilerOptions.UseMcs5_23))
				{
					preprocessorSymbols.Add("MCS5");
				}
			}
			else
			{
				preprocessorSymbols.Add("LEGACY_CSC");
				preprocessorSymbols.Add("LEGACY_VBC");
			}
			return preprocessorSymbols;
		}

		public static async Task<CompilerResults> CompileCSharp(string sourceFileName, CompilerOptions flags = CompilerOptions.UseDebug, string outputFileName = null)
		{
			List<string> sourceFileNames = new List<string> { sourceFileName };
			foreach (Match match in Regex.Matches(File.ReadAllText(sourceFileName), @"#include ""([\w\d./]+)"""))
			{
				sourceFileNames.Add(Path.GetFullPath(Path.Combine(Path.GetDirectoryName(sourceFileName), match.Groups[1].Value)));
			}
			bool targetNet40 = (flags & CompilerOptions.TargetNet40) != 0;
			bool useRoslyn = (flags & CompilerOptions.UseRoslynMask) != 0;
			if (useRoslyn && !targetNet40)
			{
				sourceFileNames.Add(targetFrameworkAttributeSnippetFile.Value);
			}

			var preprocessorSymbols = GetPreprocessorSymbols(flags);

			if ((flags & CompilerOptions.UseMcsMask) == 0)
			{
				CompilerResults results = new CompilerResults();
				results.PathToAssembly = outputFileName;

				var (roslynVersion, languageVersion) = (flags & CompilerOptions.UseRoslynMask) switch {
					0 => ("legacy", "5"),
					CompilerOptions.UseRoslyn1_3_2 => ("1.3.2", "6"),
					CompilerOptions.UseRoslyn2_10_0 => ("2.10.0", "latest"),
					CompilerOptions.UseRoslyn3_11_0 => ("3.11.0", "latest"),
					_ => (roslynLatestVersion, flags.HasFlag(CompilerOptions.Preview) ? "preview" : "latest")
				};

				var cscPath = roslynToolset.GetCSharpCompiler(roslynVersion);

				string libPath;
				IEnumerable<string> references;
				if (useRoslyn && !targetNet40)
				{
					libPath = "\"" + coreRefAsmPath + "\"";
					references = coreDefaultReferences.Select(r => "-r:\"" + Path.Combine(coreRefAsmPath, r) + "\"");
				}
				else
				{
					libPath = "\"" + RefAsmPath + "\",\"" + Path.Combine(RefAsmPath, "Facades") + "\"";
					references = defaultReferences.Select(r => "-r:\"" + Path.Combine(RefAsmPath, r) + "\"");
				}
				if (flags.HasFlag(CompilerOptions.ReferenceVisualBasic))
				{
					references = references.Concat(new[] { "-r:\"Microsoft.VisualBasic.dll\"" });
				}
				if (useRoslyn && !targetNet40 && flags.HasFlag(CompilerOptions.ReferenceUnsafe))
				{
					references = references.Concat(new[] { "-r:\"System.Runtime.CompilerServices.Unsafe.dll\"" });
				}
				string otherOptions = $"-nologo -noconfig " +
					$"-langversion:{languageVersion} " +
					$"-unsafe -o{(flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ")}";

				// note: the /shared switch is undocumented. It allows us to use the VBCSCompiler.exe compiler
				// server to speed up testing
				if (roslynVersion != "legacy")
				{
					otherOptions += "/shared ";
					if (!targetNet40 && Version.Parse(RoslynToolset.SanitizeVersion(roslynVersion)).Major > 2)
					{
						if (flags.HasFlag(CompilerOptions.NullableEnable))
							otherOptions += "/nullable+ ";
						else
							otherOptions += "/nullable- ";
					}
				}

				if (flags.HasFlag(CompilerOptions.Library))
				{
					otherOptions += "-t:library ";
				}
				else
				{
					otherOptions += "-t:exe ";
				}

				if (flags.HasFlag(CompilerOptions.GeneratePdb))
				{
					otherOptions += "-debug:full ";
				}
				else
				{
					otherOptions += "-debug- ";
				}

				if (flags.HasFlag(CompilerOptions.Force32Bit))
				{
					otherOptions += "-platform:x86 ";
				}
				else
				{
					otherOptions += "-platform:anycpu ";
				}
				if (preprocessorSymbols.Count > 0)
				{
					otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
				}

				var command = Cli.Wrap(cscPath)
					.WithArguments($"{otherOptions} -lib:{libPath} {string.Join(" ", references)} -out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}")
					.WithValidation(CommandResultValidation.None);
				//Console.WriteLine($"\"{command.TargetFilePath}\" {command.Arguments}");

				var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
				if (!string.IsNullOrWhiteSpace(result.StandardOutput))
				{
					Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
				}
				if (!string.IsNullOrWhiteSpace(result.StandardError))
				{
					Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
				}

				Assert.AreEqual(0, result.ExitCode, "csc failed");

				return results;
			}
			else
			{
				CompilerResults results = new CompilerResults();
				results.PathToAssembly = outputFileName;
				string testBasePath = RoundtripAssembly.TestDir;
				if (!Directory.Exists(testBasePath))
				{
					Assert.Ignore($"Compilation with mcs ignored: test directory '{testBasePath}' needs to be checked out separately." + Environment.NewLine +
			  $"git clone https://github.com/icsharpcode/ILSpy-tests \"{testBasePath}\"");
				}
				string mcsPath = (flags & CompilerOptions.UseMcsMask) switch {
					CompilerOptions.UseMcs5_23 => Path.Combine(testBasePath, @"mcs\5.23\bin\mcs.bat"),
					_ => Path.Combine(testBasePath, @"mcs\2.6.4\bin\gmcs.bat")
				};
				string otherOptions = " -unsafe -o" + (flags.HasFlag(CompilerOptions.Optimize) ? "+ " : "- ");

				if (flags.HasFlag(CompilerOptions.Library))
				{
					otherOptions += "-t:library ";
				}
				else
				{
					otherOptions += "-t:exe ";
				}

				if (flags.HasFlag(CompilerOptions.UseDebug))
				{
					otherOptions += "-g ";
				}

				if (flags.HasFlag(CompilerOptions.Force32Bit))
				{
					otherOptions += "-platform:x86 ";
				}
				else
				{
					otherOptions += "-platform:anycpu ";
				}
				if (preprocessorSymbols.Count > 0)
				{
					otherOptions += " \"-d:" + string.Join(";", preprocessorSymbols) + "\" ";
				}

				var command = Cli.Wrap(mcsPath)
					.WithArguments($"{otherOptions}-out:\"{Path.GetFullPath(results.PathToAssembly)}\" {string.Join(" ", sourceFileNames.Select(fn => '"' + Path.GetFullPath(fn) + '"'))}")
					.WithValidation(CommandResultValidation.None);
				//Console.WriteLine($"\"{command.TargetFilePath}\" {command.Arguments}");

				var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);

				if (!string.IsNullOrWhiteSpace(result.StandardOutput))
				{
					Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
				}
				if (!string.IsNullOrWhiteSpace(result.StandardError))
				{
					Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
				}
				Assert.AreEqual(0, result.ExitCode, "mcs failed");

				return results;
			}
		}

		internal static DecompilerSettings GetSettings(CompilerOptions cscOptions)
		{
			if ((cscOptions & CompilerOptions.UseRoslynMask) != 0)
			{
				var langVersion = (cscOptions & CompilerOptions.UseRoslynMask) switch {
					CompilerOptions.UseRoslyn1_3_2 => CSharp.LanguageVersion.CSharp6,
					CompilerOptions.UseRoslyn2_10_0 => CSharp.LanguageVersion.CSharp7_3,
					CompilerOptions.UseRoslyn3_11_0 => CSharp.LanguageVersion.CSharp9_0,
					_ => cscOptions.HasFlag(CompilerOptions.Preview) ? CSharp.LanguageVersion.Latest : CSharp.LanguageVersion.CSharp11_0,
				};
				DecompilerSettings settings = new(langVersion) {
					// Never use file-scoped namespaces
					FileScopedNamespaces = false
				};
				return settings;
			}
			else
			{
				var settings = new DecompilerSettings(CSharp.LanguageVersion.CSharp5);
				if ((cscOptions & CompilerOptions.UseMcsMask) != 0)
				{
					// we don't recompile with mcs but with roslyn, so we can use ref locals
					settings.UseRefLocalsForAccurateOrderOfEvaluation = true;
				}
				return settings;
			}
		}

		public static void CompileCSharpWithPdb(string assemblyName, Dictionary<string, string> sourceFiles)
		{
			var parseOptions = new CSharpParseOptions(languageVersion: Microsoft.CodeAnalysis.CSharp.LanguageVersion.Latest);

			List<EmbeddedText> embeddedTexts = new List<EmbeddedText>();
			List<SyntaxTree> syntaxTrees = new List<SyntaxTree>();

			foreach (KeyValuePair<string, string> file in sourceFiles)
			{
				var sourceText = SourceText.From(file.Value, new UTF8Encoding(false), SourceHashAlgorithm.Sha256);
				syntaxTrees.Add(SyntaxFactory.ParseSyntaxTree(sourceText, parseOptions, file.Key));
				embeddedTexts.Add(EmbeddedText.FromSource(file.Key, sourceText));
			}

			var compilation = CSharpCompilation.Create(Path.GetFileNameWithoutExtension(assemblyName),
				syntaxTrees, coreDefaultReferences.Select(r => MetadataReference.CreateFromFile(Path.Combine(coreRefAsmPath, r))),
				new CSharpCompilationOptions(
					OutputKind.DynamicallyLinkedLibrary,
					platform: Platform.AnyCpu,
					optimizationLevel: OptimizationLevel.Release,
					allowUnsafe: true,
					deterministic: true
				));
			using (FileStream peStream = File.Open(assemblyName + ".dll", FileMode.OpenOrCreate, FileAccess.ReadWrite))
			using (FileStream pdbStream = File.Open(assemblyName + ".pdb", FileMode.OpenOrCreate, FileAccess.ReadWrite))
			{
				var emitResult = compilation.Emit(peStream, pdbStream, options: new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb, pdbFilePath: assemblyName + ".pdb"), embeddedTexts: embeddedTexts);
				if (!emitResult.Success)
				{
					StringBuilder b = new StringBuilder("Compiler error:");
					foreach (var diag in emitResult.Diagnostics)
					{
						b.AppendLine(diag.ToString());
					}
					throw new Exception(b.ToString());
				}
			}
		}

		internal static string GetSuffix(CompilerOptions cscOptions)
		{
			string suffix = "";
			if ((cscOptions & CompilerOptions.Optimize) != 0)
				suffix += ".opt";
			if ((cscOptions & CompilerOptions.Force32Bit) != 0)
				suffix += ".32";
			if ((cscOptions & CompilerOptions.UseDebug) != 0)
				suffix += ".dbg";
			if ((cscOptions & CompilerOptions.TargetNet40) != 0)
				suffix += ".net40";
			if ((cscOptions & CompilerOptions.UseRoslyn1_3_2) != 0)
				suffix += ".roslyn1";
			if ((cscOptions & CompilerOptions.UseRoslyn2_10_0) != 0)
				suffix += ".roslyn2";
			if ((cscOptions & CompilerOptions.UseRoslyn3_11_0) != 0)
				suffix += ".roslyn3";
			if ((cscOptions & CompilerOptions.UseRoslynLatest) != 0)
				suffix += ".roslyn";
			if ((cscOptions & CompilerOptions.UseMcs2_6_4) != 0)
				suffix += ".mcs2";
			if ((cscOptions & CompilerOptions.UseMcs5_23) != 0)
				suffix += ".mcs5";
			return suffix;
		}

		public static async Task<(int ExitCode, string Output, string Error)> Run(string assemblyFileName)
		{
			var command = Cli.Wrap(assemblyFileName)
				.WithValidation(CommandResultValidation.None);

			var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);

			return (result.ExitCode, result.StandardOutput, result.StandardError);
		}

		public static async Task<(int ExitCode, string Output, string Error)> RunWithTestRunner(string assemblyFileName, bool force32Bit)
		{
			string testRunner = Path.Combine(testRunnerBasePath, force32Bit ? "win-x86" : "win-x64", "ICSharpCode.Decompiler.TestRunner.exe");
			var command = Cli.Wrap(testRunner)
				.WithArguments(assemblyFileName)
				.WithValidation(CommandResultValidation.None);

			var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);

			return (result.ExitCode, result.StandardOutput, result.StandardError);
		}

		public static Task<string> DecompileCSharp(string assemblyFileName, DecompilerSettings settings = null)
		{
			if (settings == null)
				settings = new DecompilerSettings();
			using (var file = new FileStream(assemblyFileName, FileMode.Open, FileAccess.Read))
			{
				var module = new PEFile(assemblyFileName, file, PEStreamOptions.PrefetchEntireImage);
				string targetFramework = module.Metadata.DetectTargetFrameworkId();
				var resolver = new UniversalAssemblyResolver(assemblyFileName, false,
					targetFramework, null, PEStreamOptions.PrefetchMetadata);
				resolver.AddSearchDirectory(targetFramework.Contains(".NETFramework") ? RefAsmPath : coreRefAsmPath);
				var typeSystem = new DecompilerTypeSystem(module, resolver, settings);
				CSharpDecompiler decompiler = new CSharpDecompiler(typeSystem, settings);
				decompiler.AstTransforms.Insert(0, new RemoveEmbeddedAttributes());
				decompiler.AstTransforms.Insert(0, new RemoveCompilerAttribute());
				decompiler.AstTransforms.Insert(0, new RemoveNamespaceMy());
				decompiler.AstTransforms.Add(new EscapeInvalidIdentifiers());
				var pdbFileName = Path.ChangeExtension(assemblyFileName, ".pdb");
				if (File.Exists(pdbFileName))
					decompiler.DebugInfoProvider = DebugInfoUtils.FromFile(module, pdbFileName);
				var syntaxTree = decompiler.DecompileWholeModuleAsSingleFile(sortTypes: true);

				StringWriter output = new StringWriter();
				CSharpFormattingOptions formattingPolicy = CreateFormattingPolicyForTests();
				var visitor = new CSharpOutputVisitor(output, formattingPolicy);
				syntaxTree.AcceptVisitor(visitor);

				string fileName = Path.GetTempFileName();
				File.WriteAllText(fileName, output.ToString());

				return Task.FromResult(fileName);
			}
		}

		private static CSharpFormattingOptions CreateFormattingPolicyForTests()
		{
			var formattingPolicy = FormattingOptionsFactory.CreateSharpDevelop();
			formattingPolicy.StatementBraceStyle = BraceStyle.NextLine;
			formattingPolicy.CatchNewLinePlacement = NewLinePlacement.NewLine;
			formattingPolicy.ElseNewLinePlacement = NewLinePlacement.NewLine;
			formattingPolicy.FinallyNewLinePlacement = NewLinePlacement.NewLine;
			formattingPolicy.SpaceBeforeAnonymousMethodParentheses = true;
			return formattingPolicy;
		}

		public static async Task RunAndCompareOutput(string testFileName, string outputFile, string decompiledOutputFile, string decompiledCodeFile = null, bool useTestRunner = false, bool force32Bit = false)
		{
			string output1, output2, error1, error2;
			int result1, result2;

			if (useTestRunner)
			{
				(result1, output1, error1) = await RunWithTestRunner(outputFile, force32Bit).ConfigureAwait(false);
				(result2, output2, error2) = await RunWithTestRunner(decompiledOutputFile, force32Bit).ConfigureAwait(false);
			}
			else
			{
				(result1, output1, error1) = await Run(outputFile).ConfigureAwait(false);
				(result2, output2, error2) = await Run(decompiledOutputFile).ConfigureAwait(false);
			}

			Assert.AreEqual(0, result1, "Exit code != 0; did the test case crash?" + Environment.NewLine + error1);
			Assert.AreEqual(0, result2, "Exit code != 0; did the decompiled code crash?" + Environment.NewLine + error2);

			if (output1 != output2 || error1 != error2)
			{
				StringBuilder b = new StringBuilder();
				b.AppendLine($"Test {testFileName} failed: output does not match.");
				if (decompiledCodeFile != null)
				{
					b.AppendLine($"Decompiled code in {decompiledCodeFile}:line 1");
				}
				if (error1 != error2)
				{
					b.AppendLine("Got different error output.");
					b.AppendLine("Original error:");
					b.AppendLine(error1);
					b.AppendLine();
					b.AppendLine("Error after de+re-compiling:");
					b.AppendLine(error2);
					b.AppendLine();
				}
				if (output1 != output2)
				{
					string outputFileName = Path.Combine(Path.GetTempPath(), Path.GetFileNameWithoutExtension(testFileName));
					File.WriteAllText(outputFileName + ".original.out", output1);
					File.WriteAllText(outputFileName + ".decompiled.out", output2);
					int diffLine = 0;
					string lastHeader = null;
					Tuple<string, string> errorItem = null;
					foreach (var pair in output1.Replace("\r", "").Split('\n').Zip(output2.Replace("\r", "").Split('\n'), Tuple.Create))
					{
						diffLine++;
						if (pair.Item1 != pair.Item2)
						{
							errorItem = pair;
							break;
						}
						if (pair.Item1.EndsWith(":", StringComparison.Ordinal))
						{
							lastHeader = pair.Item1;
						}
					}
					b.AppendLine($"Output differs; first difference in line {diffLine}");
					if (lastHeader != null)
					{
						b.AppendLine(lastHeader);
					}
					b.AppendLine($"{outputFileName}.original.out:line {diffLine}");
					b.AppendLine(errorItem.Item1);
					b.AppendLine($"{outputFileName}.decompiled.out:line {diffLine}");
					b.AppendLine(errorItem.Item2);
				}
				Assert.Fail(b.ToString());
			}
		}

		internal static void RepeatOnIOError(Action action, int numTries = 5)
		{
			for (int i = 0; i < numTries - 1; i++)
			{
				try
				{
					action();
					return;
				}
				catch (IOException)
				{
				}
				catch (UnauthorizedAccessException)
				{
					// potential virus scanner problem
				}
				Thread.Sleep(10);
			}
			// If the last try still fails, don't catch the exception
			action();
		}

		public static async Task SignAssembly(string assemblyPath, string keyFilePath)
		{
			string snPath = SdkUtility.GetSdkPath("sn.exe");

			var command = Cli.Wrap(snPath)
				.WithArguments($"-R \"{assemblyPath}\" \"{keyFilePath}\"")
				.WithValidation(CommandResultValidation.None);

			var result = await command.ExecuteBufferedAsync().ConfigureAwait(false);
			Assert.AreEqual(0, result.ExitCode, "sn failed");

			if (!string.IsNullOrWhiteSpace(result.StandardOutput))
			{
				Console.WriteLine("output:" + Environment.NewLine + result.StandardOutput);
			}
			if (!string.IsNullOrWhiteSpace(result.StandardError))
			{
				Console.WriteLine("errors:" + Environment.NewLine + result.StandardError);
			}
		}

		public static async Task<string> FindMSBuild()
		{
			string path = vswhereToolset.GetVsWhere();

			var result = await Cli.Wrap(path)
				.WithArguments(@"-latest -requires Microsoft.Component.MSBuild -find MSBuild\**\Bin\MSBuild.exe")
				.WithValidation(CommandResultValidation.None)
				.ExecuteBufferedAsync().ConfigureAwait(false);
			if (result.ExitCode != 0)
				throw new InvalidOperationException("Could not find MSBuild");
			return result.StandardOutput.TrimEnd();
		}
	}

	public class CompilerResults
	{
		readonly HashSet<string> tempFiles = new(Decompiler.Util.Platform.FileNameComparer);
		string pathToAssembly;

		public string PathToAssembly {
			get {
				if (pathToAssembly == null)
				{
					pathToAssembly = Path.GetTempFileName();
					tempFiles.Add(pathToAssembly);
				}
				return pathToAssembly;
			}
			set {
				if (pathToAssembly != null)
				{
					throw new InvalidOperationException("PathToAssembly can only be set once");
				}
				pathToAssembly = value;
			}
		}

		public void DeleteTempFiles()
		{
			foreach (var file in tempFiles)
			{
				Tester.RepeatOnIOError(() => File.Delete(file));
			}
		}

		public void AddTempFile(string file)
		{
			if (!Path.IsPathFullyQualified(file))
			{
				throw new InvalidOperationException("file must be a fully qualified path");
			}
			tempFiles.Add(file);
		}
	}
}
