// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Flax.Build.Platforms;

namespace Flax.Build.Projects.VisualStudio
{
    /// <summary>
    /// Project generator for Visual Studio IDE.
    /// </summary>
    public abstract class VisualStudioProjectGenerator : ProjectGenerator
    {
        private sealed class AndroidProject : VisualStudioProject
        {
            /// <inheritdoc />
            public override Guid ProjectTypeGuid => ProjectTypeGuids.Android;

            /// <inheritdoc />
            public override void Generate(string solutionPath)
            {
                // Try to reuse the existing project guid from existing files
                ProjectGuid = GetProjectGuid(Path, Name);
                if (ProjectGuid == Guid.Empty)
                    ProjectGuid = GetProjectGuid(solutionPath, Name);
                if (ProjectGuid == Guid.Empty)
                    ProjectGuid = Guid.NewGuid();

                var gen = (VisualStudioProjectGenerator)Generator;
                var projectFileToolVersion = gen.ProjectFileToolVersion;
                var vcProjectFileContent = new StringBuilder();
                vcProjectFileContent.AppendLine("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
                vcProjectFileContent.AppendLine(string.Format("<Project DefaultTargets=\"Build\" ToolsVersion=\"{0}\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">", projectFileToolVersion));
                vcProjectFileContent.AppendLine("  <ItemGroup Label=\"ProjectConfigurations\">");
                foreach (var configuration in Configurations)
                {
                    vcProjectFileContent.AppendLine(string.Format("    <ProjectConfiguration Include=\"{0}\">", configuration.Name));
                    vcProjectFileContent.AppendLine(string.Format("      <Configuration>{0}</Configuration>", configuration.Text));
                    vcProjectFileContent.AppendLine(string.Format("      <Platform>{0}</Platform>", configuration.ArchitectureName));
                    vcProjectFileContent.AppendLine("    </ProjectConfiguration>");
                }
                vcProjectFileContent.AppendLine("  </ItemGroup>");
                vcProjectFileContent.AppendLine("  <PropertyGroup Label=\"Globals\">");
                vcProjectFileContent.AppendLine(string.Format("    <ProjectGuid>{0}</ProjectGuid>", ProjectGuid.ToString("B").ToUpperInvariant()));
                vcProjectFileContent.AppendLine(string.Format("    <RootNamespace>{0}</RootNamespace>", BaseName));
                vcProjectFileContent.AppendLine(string.Format("    <MinimumVisualStudioVersion>{0}</MinimumVisualStudioVersion>", projectFileToolVersion));
                vcProjectFileContent.AppendLine(string.Format("    <AndroidAPILevel>{0}</AndroidAPILevel>", Configuration.AndroidPlatformApi));
                vcProjectFileContent.AppendLine(string.Format("    <AndroidSupportedAbis>{0}</AndroidSupportedAbis>", "arm64-v8a"));
                vcProjectFileContent.AppendLine("    <ConfigurationType>Application</ConfigurationType>");
                vcProjectFileContent.AppendLine("    <AntPackage>");
                vcProjectFileContent.AppendLine("      <AndroidAppLibName>$(RootNamespace)</AndroidAppLibName>");
                vcProjectFileContent.AppendLine("    </AntPackage>");
                vcProjectFileContent.AppendLine("  </PropertyGroup>");
                vcProjectFileContent.AppendLine("  <Import Project=\"$(AndroidTargetsPath)\\Android.Default.props\" />");
                vcProjectFileContent.AppendLine("  <Import Project=\"$(AndroidTargetsPath)\\Android.props\" />");
                vcProjectFileContent.AppendLine("  <ImportGroup Label=\"ExtensionSettings\" />");
                vcProjectFileContent.AppendLine("  <ImportGroup Label=\"Shared\" />");
                vcProjectFileContent.AppendLine("  <PropertyGroup Label=\"UserMacros\" />");
                vcProjectFileContent.AppendLine("  <PropertyGroup />");
                vcProjectFileContent.AppendLine("  <Import Project=\"$(AndroidTargetsPath)\\Android.targets\" />");
                vcProjectFileContent.AppendLine("  <ImportGroup Label=\"ExtensionTargets\" />");
                vcProjectFileContent.AppendLine("</Project>");
                Utilities.WriteFileIfChanged(Path, vcProjectFileContent.ToString());
            }
        }

        /// <summary>
        /// The Visual Studio solution project file types GUIDs. Based on http://www.codeproject.com/Reference/720512/List-of-Visual-Studio-Project-Type-GUIDs.
        /// </summary>
        public static class ProjectTypeGuids
        {
            /// <summary>
            /// The solution folder.
            /// </summary>
            public static Guid SolutionFolder = new Guid("{2150E333-8FDC-42A3-9474-1A3956D46DE8}");

            /// <summary>
            /// The Windows C#
            /// </summary>
            public static Guid WindowsCSharp = new Guid("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}");

            /// <summary>
            /// The Windows Visual C++.
            /// </summary>
            public static Guid WindowsVisualCpp = new Guid("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}");

            /// <summary>
            /// The Android.
            /// </summary>
            public static Guid Android = new Guid("{39E2626F-3545-4960-A6E8-258AD8476CE5}");

            /// <summary>
            /// The Flax.VS.
            /// </summary>
            public static Guid FlaxVS = new Guid("{BE633490-FBA4-41EB-80D4-EFA312592B8E}");

            /// <summary>
            /// Converts the GUI to To the option.
            /// </summary>
            /// <param name="projectType">Type of the project.</param>
            /// <returns>The option string.</returns>
            public static string ToOption(Guid projectType)
            {
                return projectType.ToString("B").ToUpperInvariant();
            }
        }

        /// <summary>
        /// The target IDE version.
        /// </summary>
        public readonly VisualStudioVersion Version;

        /// <summary>
        /// Gets the project file tool version.
        /// </summary>
        public string ProjectFileToolVersion
        {
            get
            {
                switch (Version)
                {
                case VisualStudioVersion.VisualStudio2015: return "14.0";
                case VisualStudioVersion.VisualStudio2017: return "15.0";
                case VisualStudioVersion.VisualStudio2019: return "16.0";
                case VisualStudioVersion.VisualStudio2022: return "17.0";
                }

                return string.Empty;
            }
        }

        /// <summary>
        /// Initializes a new instance of the <see cref="VisualStudioProjectGenerator"/> class.
        /// </summary>
        /// <param name="version">The version.</param>
        protected VisualStudioProjectGenerator(VisualStudioVersion version)
        {
            Version = version;
        }

        /// <summary>
        /// Gets the project unique identifier from the existing project or generates a new one.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>The project ID.</returns>
        public static Guid GetProjectGuid(string path, string projectName)
        {
            // Look up for the guid in VC++-project file
            if (File.Exists(path) && Path.GetExtension(path).Equals(".vcxproj", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    XmlDocument doc = new XmlDocument();
                    doc.Load(path);

                    XmlNodeList elements = doc.GetElementsByTagName("ProjectGuid");
                    foreach (XmlElement element in elements)
                    {
                        return Guid.ParseExact(element.InnerText.Trim("{}".ToCharArray()), "D");
                    }
                }
                catch
                {
                    // Hide errors
                }
            }
            if (File.Exists(path) && Path.GetExtension(path).Equals(".sln", StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    Regex projectRegex = new Regex(@"Project\(""{(\S+)}""\) = \""(\S+)\"", \""(\S+)\"", \""{(\S+)}\""");
                    MatchCollection matches = projectRegex.Matches(File.ReadAllText(path));
                    for (int i = 0; i < matches.Count; i++)
                    {
                        if (matches[i].Groups[1].Value.Equals("2150E333-8FDC-42A3-9474-1A3956D46DE8", StringComparison.OrdinalIgnoreCase))
                            continue;
                        if (matches[i].Groups[2].Value == projectName)
                            return Guid.ParseExact(matches[i].Groups[4].Value, "D");
                    }
                }
                catch
                {
                    // Hide errors
                }
            }

            return Guid.Empty;
        }

        /// <inheritdoc />
        public override string SolutionFileExtension => "sln";

        /// <inheritdoc />
        public override Project CreateProject()
        {
            return new VisualStudioProject
            {
                Generator = this,
            };
        }

        private struct SolutionConfiguration : IEquatable<SolutionConfiguration>, IComparable
        {
            public string Name;
            public string OriginalName;
            public string Configuration;
            public string Platform;

            public SolutionConfiguration(Project.ConfigurationData configuration)
            {
                OriginalName = configuration.Name.Replace("AnyCPU", "Any CPU");
                var name = OriginalName.Remove(OriginalName.IndexOf('|'));
                var parts = name.Split('.');
                if (parts.Length == 3)
                    Configuration = parts[0] + '.' + parts[2];
                else if (parts.Length == 1)
                    Configuration = parts[0];
                else
                    throw new Exception($"Unknown project configuration {configuration.Name}.");
                Platform = configuration.PlatformName + "_" + configuration.ArchitectureName;
                if (configuration.Architecture == TargetArchitecture.AnyCPU)
                    Platform = "Any CPU";
                var platform = Build.Platform.GetPlatform(configuration.Platform);
                if (platform is IProjectCustomizer customizer)
                    customizer.GetSolutionArchitectureName(configuration.Architecture, ref Platform);
                Name = Configuration + '|' + Platform;
            }

            public SolutionConfiguration(string configuration, string platform)
            {
                OriginalName = string.Empty;
                Name = configuration + '|' + platform;
                Configuration = configuration;
                Platform = platform;
            }

            /// <inheritdoc />
            public int CompareTo(object obj)
            {
                if (obj is SolutionConfiguration other)
                    return Name.CompareTo(other.Name);
                return 1;
            }

            /// <inheritdoc />
            public bool Equals(SolutionConfiguration other)
            {
                return Name == other.Name;
            }

            /// <inheritdoc />
            public override string ToString()
            {
                return Name;
            }
        }

        /// <inheritdoc />
        public override void GenerateSolution(Solution solution)
        {
            // Ensure that the main project is the first one (initially selected by Visual Studio)
            if (solution.MainProject != null && solution.Projects.Length != 0 && solution.Projects[0] != solution.MainProject)
            {
                for (int i = 1; i < solution.Projects.Length; i++)
                {
                    if (solution.Projects[i] == solution.MainProject)
                    {
                        solution.Projects[i] = solution.Projects[0];
                        solution.Projects[0] = solution.MainProject;
                        break;
                    }
                }
            }

            // Try to extract solution folder info from the existing solution file to make random IDs stable
            var solutionId = Guid.NewGuid();
            var folderIds = new Dictionary<string, Guid>();
            if (File.Exists(solution.Path))
            {
                try
                {
                    var contents = File.ReadAllText(solution.Path);

                    var solutionIdMatch = Regex.Match(contents, "SolutionGuid = \\{(.*?)\\}");
                    if (solutionIdMatch.Success)
                    {
                        var value = solutionIdMatch.Value;
                        solutionId = Guid.ParseExact(value.Substring(15), "B");
                    }

                    var folderIdMatches = new Regex("Project\\(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\"\\) = \"(.*?)\", \"(.*?)\", \"{(.*?)}\"").Matches(contents);
                    foreach (Match match in folderIdMatches)
                    {
                        var folder = match.Groups[2].Value;
                        var folderId = Guid.ParseExact(match.Groups[3].Value, "D");
                        folderIds[folder] = folderId;
                    }
                }
                catch (Exception ex)
                {
                    Log.Warning("Failed to restore solution and solution folders identifiers from existing file.");
                    Log.Exception(ex);
                }
            }

            StringBuilder vcSolutionFileContent = new StringBuilder();
            var solutionDirectory = Path.GetDirectoryName(solution.Path);
            var projects = solution.Projects.Cast<VisualStudioProject>().ToArray();

            // Header
            if (Version == VisualStudioVersion.VisualStudio2022)
            {
                vcSolutionFileContent.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00");
                vcSolutionFileContent.AppendLine("# Visual Studio Version 17");
                vcSolutionFileContent.AppendLine("VisualStudioVersion = 17.0.31314.256");
                vcSolutionFileContent.AppendLine("MinimumVisualStudioVersion = 10.0.40219.1");
            }
            else if (Version == VisualStudioVersion.VisualStudio2019)
            {
                vcSolutionFileContent.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00");
                vcSolutionFileContent.AppendLine("# Visual Studio Version 16");
                vcSolutionFileContent.AppendLine("VisualStudioVersion = 16.0.28315.86");
                vcSolutionFileContent.AppendLine("MinimumVisualStudioVersion = 10.0.40219.1");
            }
            else if (Version == VisualStudioVersion.VisualStudio2017)
            {
                vcSolutionFileContent.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00");
                vcSolutionFileContent.AppendLine("# Visual Studio 15");
                vcSolutionFileContent.AppendLine("VisualStudioVersion = 15.0.25807.0");
                vcSolutionFileContent.AppendLine("MinimumVisualStudioVersion = 10.0.40219.1");
            }
            else if (Version == VisualStudioVersion.VisualStudio2015)
            {
                vcSolutionFileContent.AppendLine("Microsoft Visual Studio Solution File, Format Version 12.00");
                vcSolutionFileContent.AppendLine("# Visual Studio 14");
                vcSolutionFileContent.AppendLine("VisualStudioVersion = 14.0.22310.1");
                vcSolutionFileContent.AppendLine("MinimumVisualStudioVersion = 10.0.40219.1");
            }
            else
            {
                throw new Exception("Unsupported solution file format.");
            }

            // Solution folders
            var folderNames = new HashSet<string>();
            {
                // Move projects to subfolders based on group names including subfolders to match the location of the source in the workspace
                foreach (var project in projects)
                {
                    var folder = project.GroupName;

                    if (project.SourceDirectories != null && project.SourceDirectories.Count == 1)
                    {
                        var subFolder = Utilities.MakePathRelativeTo(Path.GetDirectoryName(project.SourceDirectories[0]), project.WorkspaceRootPath);
                        if (subFolder.StartsWith("Source\\"))
                            subFolder = subFolder.Substring(7);
                        if (subFolder.Length != 0)
                        {
                            if (folder.Length != 0)
                                folder += '\\';
                            folder += subFolder;
                        }
                    }

                    if (string.IsNullOrEmpty(folder))
                        continue;

                    var folderParents = folder.Split('\\');
                    for (int i = 0; i < folderParents.Length; i++)
                    {
                        var folderPath = folderParents[0];
                        for (int j = 1; j <= i; j++)
                            folderPath += '\\' + folderParents[j];

                        if (folderNames.Contains(folderPath))
                        {
                            project.FolderGuid = folderIds[folderPath];
                        }
                        else
                        {
                            if (!folderIds.TryGetValue(folderPath, out project.FolderGuid))
                            {
                                project.FolderGuid = Guid.NewGuid();
                                folderIds.Add(folderPath, project.FolderGuid);
                            }
                            folderNames.Add(folderPath);
                        }
                    }
                }

                foreach (var folder in folderNames)
                {
                    var folderGuid = folderIds[folder].ToString("B").ToUpperInvariant();
                    var typeGuid = ProjectTypeGuids.ToOption(ProjectTypeGuids.SolutionFolder);
                    var lastSplit = folder.LastIndexOf('\\');
                    var name = lastSplit != -1 ? folder.Substring(lastSplit + 1) : folder;

                    vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, name, folder, folderGuid));
                    vcSolutionFileContent.AppendLine("EndProject");
                }
            }

            // Solution projects
            foreach (var project in projects)
            {
                var projectId = project.ProjectGuid.ToString("B").ToUpperInvariant();
                var typeGuid = ProjectTypeGuids.ToOption(project.ProjectTypeGuid);

                vcSolutionFileContent.AppendLine(string.Format("Project(\"{0}\") = \"{1}\", \"{2}\", \"{3}\"", typeGuid, project.Name, Utilities.MakePathRelativeTo(project.Path, solutionDirectory), projectId));

                if (project.Dependencies.Count > 0)
                {
                    vcSolutionFileContent.AppendLine("\tProjectSection(ProjectDependencies) = postProject");
                    foreach (var dependency in project.Dependencies.Cast<VisualStudioProject>())
                    {
                        string dependencyId = dependency.ProjectGuid.ToString("B").ToUpperInvariant();
                        vcSolutionFileContent.AppendLine("\t\t" + dependencyId + " = " + dependencyId);
                    }

                    vcSolutionFileContent.AppendLine("\tEndProjectSection");
                }

                vcSolutionFileContent.AppendLine("EndProject");
            }

            // Global configuration
            {
                vcSolutionFileContent.AppendLine("Global");

                // Collect all unique configurations
                var configurations = new HashSet<SolutionConfiguration>();
                var mainArchitectures = solution.MainProject?.Targets?.SelectMany(x => x.Architectures).Distinct().ToArray();
                foreach (var project in projects)
                {
                    if (project.Configurations == null || project.Configurations.Count == 0)
                        throw new Exception("Missing configurations for project " + project.Name);

                    // Prevent generating default Debug|AnyCPU and Release|AnyCPU configurations from Flax projects
                    if (project.Name == "BuildScripts" || project.Name == "Flax.Build" || project.Name == "Flax.Build.Tests")
                        continue;

                    foreach (var configuration in project.Configurations)
                    {
                        // Skip architectures which are not included in the game project
                        if (mainArchitectures != null && !mainArchitectures.Contains(configuration.Architecture))
                            continue;

                        configurations.Add(new SolutionConfiguration(configuration));
                    }
                }

                // Add missing configurations (Visual Studio needs all permutations of configuration/platform pair)
                var configurationNames = configurations.Select(x => x.Configuration).Distinct().ToArray();
                var platformNames = configurations.Select(x => x.Platform).Distinct().ToArray();
                foreach (var configurationName in configurationNames)
                {
                    foreach (var platformName in platformNames)
                    {
                        configurations.Add(new SolutionConfiguration(configurationName, platformName));
                    }
                }

                // Sort configurations
                var configurationsSorted = new List<SolutionConfiguration>(configurations);
                configurationsSorted.Sort();

                // Global configurations
                {
                    vcSolutionFileContent.AppendLine("	GlobalSection(SolutionConfigurationPlatforms) = preSolution");

                    foreach (var configuration in configurationsSorted)
                    {
                        vcSolutionFileContent.AppendLine("		" + configuration.Name + " = " + configuration.Name);
                    }

                    vcSolutionFileContent.AppendLine("	EndGlobalSection");
                }

                // Per-project configurations mapping
                {
                    vcSolutionFileContent.AppendLine("	GlobalSection(ProjectConfigurationPlatforms) = postSolution");

                    foreach (var project in projects)
                    {
                        string projectId = project.ProjectGuid.ToString("B").ToUpperInvariant();

                        foreach (var configuration in configurationsSorted)
                        {
                            SolutionConfiguration projectConfiguration;
                            bool build = false;
                            int firstFullMatch = -1, firstPlatformMatch = -1, firstEditorMatch = -1;
                            for (int i = 0; i < project.Configurations.Count; i++)
                            {
                                var e = new SolutionConfiguration(project.Configurations[i]);
                                if (e.Name == configuration.Name)
                                {
                                    firstFullMatch = i;
                                    break;
                                }
                                if (firstPlatformMatch == -1 && e.Platform == configuration.Platform)
                                {
                                    firstPlatformMatch = i;
                                }
                                if (firstEditorMatch == -1 && e.Configuration == configuration.Configuration)
                                {
                                    firstEditorMatch = i;
                                }
                            }
                            if (project is AndroidProject)
                            {
                                // Utility Android deploy project only for exact match
                                if (firstFullMatch != -1)
                                    projectConfiguration = configuration;
                                else
                                    projectConfiguration = new SolutionConfiguration(project.Configurations[0]);
                            }
                            else if (firstFullMatch != -1)
                            {
                                projectConfiguration = configuration;

                                // Always build the main project
                                build = solution.MainProject == project;

                                // Build C# projects (needed for Rider solution wide analysis)
                                build |= project.Type == TargetType.DotNetCore;

                                // Always build the project named after solution if main project was not set
                                build |= solution.MainProject == null && project.Name == solution.Name;
                            }
                            else if (firstPlatformMatch != -1 && !configuration.Name.StartsWith("Editor."))
                            {
                                // No exact match, pick the first configuration for matching platform
                                projectConfiguration = new SolutionConfiguration(project.Configurations[firstPlatformMatch]);
                            }
                            else if (firstEditorMatch != -1 && configuration.Name.StartsWith("Editor."))
                            {
                                // No exact match, pick the matching editor configuration for different platform.
                                // As an example, Editor configuration for Android projects should be remapped
                                // to desktop platform in order to provide working Intellisense information.
                                projectConfiguration = new SolutionConfiguration(project.Configurations[firstEditorMatch]);
                            }
                            else
                            {
                                // No match
                                projectConfiguration = new SolutionConfiguration(project.Configurations[0]);
                            }

                            vcSolutionFileContent.AppendLine(string.Format("		{0}.{1}.ActiveCfg = {2}", projectId, configuration.Name, projectConfiguration.OriginalName));
                            if (build)
                                vcSolutionFileContent.AppendLine(string.Format("		{0}.{1}.Build.0 = {2}", projectId, configuration.Name, projectConfiguration.OriginalName));
                        }
                    }

                    vcSolutionFileContent.AppendLine("	EndGlobalSection");
                }

                // Always show solution root node
                {
                    vcSolutionFileContent.AppendLine("	GlobalSection(SolutionProperties) = preSolution");
                    vcSolutionFileContent.AppendLine("		HideSolutionNode = FALSE");
                    vcSolutionFileContent.AppendLine("	EndGlobalSection");
                }

                // Solution directory hierarchy
                {
                    vcSolutionFileContent.AppendLine("	GlobalSection(NestedProjects) = preSolution");

                    // Write nested folders hierarchy
                    foreach (var folder in folderNames)
                    {
                        var lastSplit = folder.LastIndexOf('\\');
                        if (lastSplit != -1)
                        {
                            var folderGuid = folderIds[folder].ToString("B").ToUpperInvariant();
                            var parentFolder = folder.Substring(0, lastSplit);
                            var parentFolderGuid = folderIds[parentFolder].ToString("B").ToUpperInvariant();
                            vcSolutionFileContent.AppendLine(string.Format("		{0} = {1}", folderGuid, parentFolderGuid));
                        }
                    }

                    // Write mapping for projectId - folderId
                    foreach (var project in projects)
                    {
                        if (project.FolderGuid != Guid.Empty)
                        {
                            var projectGuidString = project.ProjectGuid.ToString("B").ToUpperInvariant();
                            var folderGuidString = project.FolderGuid.ToString("B").ToUpperInvariant();
                            vcSolutionFileContent.AppendLine(string.Format("		{0} = {1}", projectGuidString, folderGuidString));
                        }
                    }

                    vcSolutionFileContent.AppendLine("	EndGlobalSection");
                }

                // Solution identifier
                {
                    vcSolutionFileContent.AppendLine("	GlobalSection(ExtensibilityGlobals) = postSolution");
                    vcSolutionFileContent.AppendLine(string.Format("		SolutionGuid = {0}", solutionId.ToString("B").ToUpperInvariant()));
                    vcSolutionFileContent.AppendLine("	EndGlobalSection");
                }

                vcSolutionFileContent.AppendLine("EndGlobal");
            }

            // Save the file
            Utilities.WriteFileIfChanged(solution.Path, vcSolutionFileContent.ToString());

            // Generate launch profiles for C# projects
            if (Version >= VisualStudioVersion.VisualStudio2022)
            {
                var profiles = new Dictionary<string, string>();
                var profile = new StringBuilder();
                var editorPath = Utilities.NormalizePath(Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), $"Development/FlaxEditor{Utilities.GetPlatformExecutableExt()}")).Replace('\\', '/');
                var workspacePath = Utilities.NormalizePath(solutionDirectory).Replace('\\', '/');
                foreach (var project in projects)
                {
                    if (project.Type == TargetType.DotNetCore)
                    {
                        var path = Path.Combine(Path.GetDirectoryName(project.Path), "Properties/launchSettings.json");
                        path = Utilities.NormalizePath(path);
                        if (profiles.ContainsKey(path))
                            profile.AppendLine(",");
                        profile.AppendLine($"    \"{project.BaseName}\": {{");
                        profile.AppendLine("      \"commandName\": \"Executable\",");
                        profile.AppendLine($"      \"workingDirectory\": \"{workspacePath}\",");
                        profile.AppendLine($"      \"executablePath\": \"{editorPath}\",");
                        profile.AppendLine($"      \"commandLineArgs\": \"-project \\\"{workspacePath}\\\"\",");
                        profile.AppendLine("      \"nativeDebugging\": false");
                        profile.Append("    }");
                        if (profiles.ContainsKey(path))
                            profiles[path] += profile.ToString();
                        else
                            profiles.Add(path, profile.ToString());
                        profile.Clear();
                    }
                }
                foreach (var e in profiles)
                {
                    var folder = Path.GetDirectoryName(e.Key);
                    if (!Directory.Exists(folder))
                        Directory.CreateDirectory(folder);
                    profile.Clear();
                    profile.AppendLine("{");
                    profile.AppendLine("  \"profiles\": {");
                    profile.AppendLine(e.Value);
                    profile.AppendLine("  }");
                    profile.Append("}");
                    File.WriteAllText(e.Key, profile.ToString(), Encoding.UTF8);
                }
            }

            // Generate Rider-specific configuration files
            {
                StringBuilder dotSettingsFileContent = new StringBuilder();
                string dotSettingsUserFilePath = solution.Path + ".DotSettings.user";

                // Solution settings (user layer)
                bool useResharperBuild = false; // This needs to be disabled for custom build steps to run properly

                if (File.Exists(dotSettingsUserFilePath))
                {
                    foreach (var line in File.ReadAllLines(dotSettingsUserFilePath))
                    {
                        if (line.Contains(@"/UseMsbuildSolutionBuilder/@EntryValue"))
                        {
                            if (!useResharperBuild)
                            {
                                dotSettingsFileContent.Append("\t").Append(@"<s:String x:Key=""/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue"">No</s:String>");
                                if (line.Contains("</wpf:ResourceDictionary>"))
                                    dotSettingsFileContent.Append("</wpf:ResourceDictionary>\n");
                                else
                                    dotSettingsFileContent.Append("\n");
                            }
                            continue;
                        }
                        dotSettingsFileContent.Append(line).Append("\n");
                    }
                }
                else
                {
                    dotSettingsFileContent.Append(@"<wpf:ResourceDictionary xml:space=""preserve"" xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml"" xmlns:s=""clr-namespace:System;assembly=mscorlib"" xmlns:ss=""urn:shemas-jetbrains-com:settings-storage-xaml"" xmlns:wpf=""http://schemas.microsoft.com/winfx/2006/xaml/presentation"">").Append("\n");
                    if (!useResharperBuild)
                        dotSettingsFileContent.Append("\t").Append(@"<s:String x:Key=""/Default/Environment/Hierarchy/Build/SolBuilderDuo/UseMsbuildSolutionBuilder/@EntryValue"">No</s:String>");
                    dotSettingsFileContent.Append("</wpf:ResourceDictionary>\n");
                }

                Utilities.WriteFileIfChanged(dotSettingsUserFilePath, dotSettingsFileContent.ToString());
            }

            // Custom MSBuild .targets file to prevent building Flax C#-projects directly with MSBuild
            {
                var targetsFileContent = new StringBuilder();
                targetsFileContent.AppendLine("<Project>");
                targetsFileContent.AppendLine("  <!-- Prevent building projects with MSBuild, let Flax.Build handle the building process -->");
                targetsFileContent.AppendLine("  <Target Name=\"Build\" Condition=\"'false' == 'true'\" />");
                targetsFileContent.AppendLine("</Project>");

                Utilities.WriteFileIfChanged(Path.Combine(Globals.Root, "Cache", "Projects", "Flax.Build.CSharp.SkipBuild.targets"), targetsFileContent.ToString());
            }

            // Override MSBuild build tasks to run Flax.Build in C#-only projects
            {
                // Build command for the build tool
                var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null);

                var targetsFileContent = new StringBuilder();
                targetsFileContent.AppendLine("<Project>");
                targetsFileContent.AppendLine("  <!-- Custom Flax.Build scripts for C# projects. -->");
                targetsFileContent.AppendLine("  <Target Name=\"Build\">");
                AppendBuildToolCommands(targetsFileContent, "-build");
                targetsFileContent.AppendLine("  </Target>");
                targetsFileContent.AppendLine("  <Target Name=\"Rebuild\">");
                AppendBuildToolCommands(targetsFileContent, "-rebuild");
                targetsFileContent.AppendLine("  </Target>");
                targetsFileContent.AppendLine("  <Target Name=\"Clean\">");
                AppendBuildToolCommands(targetsFileContent, "-clean");
                targetsFileContent.AppendLine("  </Target>");
                targetsFileContent.AppendLine("</Project>");

                Utilities.WriteFileIfChanged(Path.Combine(Globals.Root, "Cache", "Projects", "Flax.Build.CSharp.targets"), targetsFileContent.ToString());

                void AppendBuildToolCommands(StringBuilder str, string extraArgs)
                {
                    if (solution.MainProject == null)
                        return;
                    foreach (var configuration in solution.MainProject.Configurations)
                    {
                        var cmdLine = string.Format("\"{0}\" -log -mutex -workspace=\"{1}\" -arch={2} -configuration={3} -platform={4} -buildTargets={5}",
                                                buildToolPath,
                                                solution.MainProject.WorkspaceRootPath,
                                                configuration.Architecture,
                                                configuration.Configuration,
                                                configuration.Platform,
                                                configuration.Target);
                        Configuration.PassArgs(ref cmdLine);

                        str.AppendLine(string.Format("    <Exec Command='{0} {1}' Condition=\"'$(Configuration)|$(Platform)'=='{2}'\"/>", cmdLine, extraArgs, configuration.Name));
                    }
                }
            }
        }

        /// <inheritdoc />
        public override void GenerateCustomProjects(List<Project> projects)
        {
            base.GenerateCustomProjects(projects);

            // Generate custom project for Visual Studio project to debug native code on Android
            var androidProject = projects.FirstOrDefault(p => p.Configurations.Any(c => c.Platform == TargetPlatform.Android));
            if (androidProject != null && AndroidSdk.Instance.IsValid && AndroidNdk.Instance.IsValid)
            {
                var platform = Platform.GetPlatform(TargetPlatform.Android);
                var rootProject = Globals.Project;
                var workspaceRoot = rootProject.ProjectFolderPath;
                var project = new AndroidProject
                {
                    Generator = this,
                };
                var targetsSorted = new List<Target>(androidProject.Targets);
                targetsSorted.Sort((a, b) => b.Platforms.Length.CompareTo(a.Platforms.Length));
                var target = targetsSorted.First(x => x.Platforms.Contains(TargetPlatform.Android));
                project.Type = TargetType.NativeCpp;
                project.Name = project.BaseName = "Android";
                project.Targets = new Target[0];
                project.SearchPaths = new string[0];
                project.WorkspaceRootPath = rootProject.ProjectFolderPath;
                project.Path = Path.Combine(workspaceRoot, "Cache", "Projects", project.Name + ".androidproj");
                project.SourceDirectories = new List<string>();
                project.Configurations = new List<Project.ConfigurationData>
                {
                    new Project.ConfigurationData(target, androidProject, platform, TargetArchitecture.ARM64, TargetConfiguration.Debug),
                    new Project.ConfigurationData(target, androidProject, platform, TargetArchitecture.ARM64, TargetConfiguration.Development),
                    new Project.ConfigurationData(target, androidProject, platform, TargetArchitecture.ARM64, TargetConfiguration.Release),
                };
                projects.Add(project);
            }
        }
    }
}
