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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using Flax.Build.NativeCpp;
using Flax.Build.Projects;

namespace Flax.Build
{
    partial class Builder
    {
        private class ProjectTargetsGroup
        {
            public string ProjectName;
            public ProjectInfo Project;
            public TargetType Type;
            public List<Target> Targets;
            public HashSet<ProjectInfo> ProjectDependencies;

            public override string ToString()
            {
                if (Project != null)
                    return $"{ProjectName}: {Project.ProjectPath}";
                return ProjectName;
            }
        }

        private static void SetupProjectConfigurations(Project project, ProjectInfo projectInfo)
        {
            project.Configurations.Clear();

            // Generate a set of configurations to build all targets for all platforms/configurations/architectures
            var rules = GenerateRulesAssembly();
            foreach (var target in project.Targets)
            {
                string targetName = target.ConfigurationName ?? target.Name;

                // When using custom project file try to extract configurations from there
                if (!string.IsNullOrEmpty(target.CustomExternalProjectFilePath))
                {
                    // TODO: implement it in a proper way by parsing the project file to extract the configurations
                    if (target.CustomExternalProjectFilePath.EndsWith(".csproj"))
                    {
                        var targetPlatform = TargetPlatform.Windows;
                        var platformName = targetPlatform.ToString();
                        var platform = Platform.BuildPlatform;
                        var architecture = TargetArchitecture.AnyCPU;
                        var architectureName = "AnyCPU";

                        if (!platform.CanBuildArchitecture(architecture))
                            continue;
                        var toolchain = platform.TryGetToolchain(architecture);

                        var configuration = TargetConfiguration.Debug;
                        var configurationName = "Debug";
                        var configurationText = configurationName;
                        var targetBuildOptions = GetBuildOptions(target, platform, toolchain, architecture, configuration, project.WorkspaceRootPath);
                        targetBuildOptions.Flags |= BuildFlags.GenerateProject;
                        var modules = CollectModules(rules, platform, target, targetBuildOptions, toolchain, architecture, configuration);
                        foreach (var module in modules)
                        {
                            // This merges private module build options into global target - not the best option but helps with syntax highlighting and references collecting
                            module.Key.Setup(targetBuildOptions);
                            module.Key.SetupEnvironment(targetBuildOptions);
                        }
                        project.Configurations.Add(new Project.ConfigurationData
                        {
                            Name = configurationText + '|' + architectureName,
                            Text = configurationText,
                            Platform = targetPlatform,
                            PlatformName = platformName,
                            Architecture = architecture,
                            ArchitectureName = architectureName,
                            Configuration = configuration,
                            ConfigurationName = configurationName,
                            Target = target,
                            TargetBuildOptions = targetBuildOptions,
                            Modules = modules,
                        });
                        configuration = TargetConfiguration.Release;
                        configurationName = "Release";
                        configurationText = configurationName;
                        targetBuildOptions = GetBuildOptions(target, platform, toolchain, architecture, configuration, project.WorkspaceRootPath);
                        targetBuildOptions.Flags |= BuildFlags.GenerateProject;
                        modules = CollectModules(rules, platform, target, targetBuildOptions, toolchain, architecture, configuration);
                        foreach (var module in modules)
                        {
                            // This merges private module build options into global target - not the best option but helps with syntax highlighting and references collecting
                            module.Key.Setup(targetBuildOptions);
                            module.Key.SetupEnvironment(targetBuildOptions);
                        }
                        project.Configurations.Add(new Project.ConfigurationData
                        {
                            Name = configurationText + '|' + architectureName,
                            Text = configurationText,
                            Platform = targetPlatform,
                            PlatformName = platformName,
                            Architecture = architecture,
                            ArchitectureName = architectureName,
                            Configuration = configuration,
                            ConfigurationName = configurationName,
                            Target = target,
                            TargetBuildOptions = targetBuildOptions,
                            Modules = modules,
                        });
                    }
                    else
                        throw new NotImplementedException();
                    continue;
                }

                foreach (var targetPlatform in target.Platforms)
                {
                    string platformName = targetPlatform.ToString();
                    foreach (var configuration in target.Configurations)
                    {
                        string configurationName = configuration.ToString();
                        foreach (var architecture in target.GetArchitectures(targetPlatform))
                        {
                            string configurationText = targetName + '.' + platformName + '.' + configurationName;
                            if (!Platform.IsPlatformSupported(targetPlatform, architecture))
                                continue;
                            var platform = Platform.GetPlatform(targetPlatform, true);
                            if (platform == null)
                                continue;
                            if (!platform.HasRequiredSDKsInstalled && (!projectInfo.IsCSharpOnlyProject || platform != Platform.BuildPlatform))
                            {
                                Log.Verbose($"Skipping configuration {configurationText} for {architecture}: Missing platform SDK");
                                continue;
                            }
                            if (!projectInfo.IsCSharpOnlyProject && !platform.CanBuildArchitecture(architecture))
                            {
                                Log.Verbose($"Skipping configuration {configurationText} for {architecture}: Unsupported target architecture");
                                continue;
                            }
                            if (projectInfo.IsCSharpOnlyProject && !Platform.IsPlatformSupported(platform.Target, architecture))
                            {
                                Log.Verbose($"Skipping configuration {configurationText} for {architecture}: Unsupported target architecture");
                                continue;
                            }

                            string architectureName = architecture.ToString();
                            if (platform is IProjectCustomizer customizer)
                                customizer.GetProjectArchitectureName(project, platform, architecture, ref architectureName);

                            var toolchain = platform.TryGetToolchain(architecture);
                            var targetBuildOptions = GetBuildOptions(target, platform, toolchain, architecture, configuration, project.WorkspaceRootPath);
                            targetBuildOptions.Flags |= BuildFlags.GenerateProject;
                            var modules = CollectModules(rules, platform, target, targetBuildOptions, toolchain, architecture, configuration);
                            foreach (var module in modules)
                            {
                                // This merges private module build options into global target - not the best option but helps with syntax highlighting and references collecting
                                module.Key.Setup(targetBuildOptions);
                                module.Key.SetupEnvironment(targetBuildOptions);
                            }

                            project.Configurations.Add(new Project.ConfigurationData
                            {
                                Name = configurationText + '|' + architectureName,
                                Text = configurationText,
                                Platform = targetPlatform,
                                PlatformName = platformName,
                                Architecture = architecture,
                                ArchitectureName = architectureName,
                                Configuration = configuration,
                                ConfigurationName = configurationName,
                                Target = target,
                                TargetBuildOptions = targetBuildOptions,
                                Modules = modules,
                            });
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Custom projects generation callback.
        /// </summary>
        public static event Action<List<Project>> GenerateCustomProjects;

        /// <summary>
        /// Generates the projects for the targets and the solution for the workspace.
        /// </summary>
        public static void GenerateProjects()
        {
            using (new ProfileEventScope("GenerateProjects"))
            {
                // Pick the project format
                var projectFormats = new HashSet<ProjectFormat>();
                if (Configuration.ProjectFormatVS2026)
                    projectFormats.Add(ProjectFormat.VisualStudio2026);
                if (Configuration.ProjectFormatVS2022)
                    projectFormats.Add(ProjectFormat.VisualStudio2022);
                if (Configuration.ProjectFormatVS2019)
                    projectFormats.Add(ProjectFormat.VisualStudio2019);
                if (Configuration.ProjectFormatVS2017)
                    projectFormats.Add(ProjectFormat.VisualStudio2017);
                if (Configuration.ProjectFormatVS2015)
                    projectFormats.Add(ProjectFormat.VisualStudio2015);
                if (Configuration.ProjectFormatVSCode)
                    projectFormats.Add(ProjectFormat.VisualStudioCode);
                if (Configuration.ProjectFormatCLion)
                    projectFormats.Add(ProjectFormat.CMake);
                if (Configuration.ProjectFormatRider)
                    projectFormats.Add(ProjectFormat.VisualStudio2022);
                if (!string.IsNullOrEmpty(Configuration.ProjectFormatCustom))
                    projectFormats.Add(ProjectFormat.Custom);
                if (projectFormats.Count == 0)
                    projectFormats.Add(Platform.BuildPlatform.DefaultProjectFormat);

                // Always generate VS solution files for project (needed for C# Intellisense support in other IDEs)
                if (!projectFormats.Contains(ProjectFormat.VisualStudio2026) &&
                    !projectFormats.Contains(ProjectFormat.VisualStudio2022) &&
                    !projectFormats.Contains(ProjectFormat.VisualStudio))
                {
                    projectFormats.Add(ProjectFormat.VisualStudio2022);
                }

                foreach (ProjectFormat projectFormat in projectFormats)
                    GenerateProject(projectFormat);
            }
        }

        /// <summary>
        /// Generates project files for the specified format.
        /// </summary>
        public static void GenerateProject(ProjectFormat projectFormat)
        {
            using (new ProfileEventScope("GenerateProject" + projectFormat))
            {
                // Setup
                var rules = GenerateRulesAssembly();
                var rootProject = Globals.Project;
                if (rootProject == null)
                    throw new Exception("Missing project.");
                var projectFiles = rootProject.GetAllProjects();
                var workspaceRoot = rootProject.ProjectFolderPath;
                var projectsRoot = Path.Combine(workspaceRoot, "Cache", "Projects");
                var projects = new List<Project>();
                var dotNetProjectGenerator = ProjectGenerator.Create(projectFormat, TargetType.DotNetCore);
                var projectToBinaryModule = new Dictionary<Project, KeyValuePair<string, HashSet<Module>>>();
                var projectToModulesBuildOptions = new Dictionary<Project, Dictionary<Module, BuildOptions>>();
                Project mainSolutionProject = null;
                ProjectGenerator nativeProjectGenerator = ProjectGenerator.Create(projectFormat, TargetType.NativeCpp);
                var solutionName = rootProject.Name;
                var solutionPath = Path.Combine(workspaceRoot, solutionName + '.' + nativeProjectGenerator.SolutionFileExtension);

                // Group targets by project name and sort groups based on the project (ensures that referenced plugin source projects are generated firstly before main source projects)
                var targetGroups = new List<ProjectTargetsGroup>();
                foreach (var target in rules.Targets)
                {
                    int i = 0;
                    for (; i < targetGroups.Count; i++)
                    {
                        if (targetGroups[i].ProjectName == target.ProjectName)
                            break;
                    }
                    if (i == targetGroups.Count)
                    {
                        targetGroups.Add(new ProjectTargetsGroup
                        {
                            ProjectName = target.ProjectName,
                            Type = target.Type,
                            Targets = new List<Target>(),
                        });
                    }
                    var targetGroup = targetGroups[i];
                    if (targetGroup.Type != target.Type)
                        Log.Error(string.Format($"Invalid targets group. Project {target.ProjectName} uses type {target.Type} from target {targetGroup.Targets[0].Name} but target {target.Name} is type of {target.Type}"));
                    if (targetGroup.Project == null && target is ProjectTarget projectTarget)
                        targetGroup.Project = projectTarget.Project;
                    targetGroup.Targets.Add(target);
                    Log.Verbose($"Found target {target}");
                }
                foreach (var targetGroup in targetGroups)
                {
                    if (targetGroup.Project == null)
                    {
                        targetGroup.Project = projectFiles.First(x => targetGroup.Targets[0].FolderPath.Contains(x.ProjectFolderPath));
                        if (targetGroup.Project == null)
                        {
                            Log.Error(string.Format($"Invalid target {targetGroup.ProjectName} has no project to link."));
                            return;
                        }
                    }
                    targetGroup.ProjectDependencies = targetGroup.Project.GetAllProjects();
                    targetGroup.ProjectDependencies.Remove(targetGroup.Project);
                }
                targetGroups.Sort((a, b) => a.ProjectDependencies.Count - b.ProjectDependencies.Count);

                // Setup projects for target groups (before actual generation to handle cross-project references like)
                foreach (var e in targetGroups)
                {
                    var projectName = e.ProjectName;
                    using (new ProfileEventScope(projectName))
                    {
                        var targets = e.Targets.ToArray();
                        var type = e.Type;
                        var projectInfo = e.Project;

                        // Create project
                        Log.Verbose($"Found project {projectName}");
                        Project mainProject;
                        var binaryModules = new Dictionary<string, HashSet<Module>>();
                        var modulesBuildOptions = new Dictionary<Module, BuildOptions>();
                        using (new ProfileEventScope("CreateProject"))
                        {
                            var generator = ProjectGenerator.Create(projectFormat, type);
                            var project = mainProject = generator.CreateProject();
                            if (targets[0].CustomExternalProjectFilePath == null)
                                project.Type = TargetType.NativeCpp;
                            project.Name = project.BaseName = projectName;
                            project.Targets = targets;
                            project.SearchPaths = Array.Empty<string>();
                            project.WorkspaceRootPath = projectInfo.ProjectFolderPath;
                            if (targets[0].CustomExternalProjectFilePath == null)
                                project.Path = Path.Combine(projectsRoot, project.Name + '.' + generator.ProjectFileExtension);
                            else
                                project.Path = targets[0].CustomExternalProjectFilePath;
                            if (project.WorkspaceRootPath.StartsWith(rootProject.ProjectFolderPath))
                                project.GroupName = Utilities.NormalizePath(Utilities.MakePathRelativeTo(project.WorkspaceRootPath, rootProject.ProjectFolderPath));
                            else if (projectInfo != Globals.Project)
                                project.GroupName = projectInfo.Name;
                            project.SourceDirectories = new List<string>
                            {
                                Path.GetDirectoryName(targets[0].FilePath)
                            };
                            SetupProjectConfigurations(project, projectInfo);

                            // Get all modules aggregated into all binary modules used in all configurations of this target
                            foreach (var configurationData in project.Configurations)
                            {
                                var configurationBinaryModules = GetBinaryModules(projectInfo, configurationData.Target, configurationData.Modules);
                                foreach (var configurationBinaryModule in configurationBinaryModules)
                                {
                                    // Skip if none of the included binary modules is inside the project workspace (eg. merged external binary modules from engine to game project)
                                    if (!configurationBinaryModule.Any(y => y.FolderPath.StartsWith(project.WorkspaceRootPath)))
                                        continue;

                                    if (!binaryModules.TryGetValue(configurationBinaryModule.Key, out var modules))
                                        binaryModules[configurationBinaryModule.Key] = modules = new HashSet<Module>();
                                    modules.AddRange(configurationBinaryModule);
                                    foreach (var module in configurationBinaryModule)
                                    {
                                        if (!modulesBuildOptions.ContainsKey(module))
                                            modulesBuildOptions.Add(module, configurationData.Modules[module]);
                                    }
                                }

                                foreach (var reference in projectInfo.References)
                                {
                                    var referenceTargets = GetProjectTargets(reference.Project);
                                    foreach (var referenceTarget in referenceTargets)
                                    {
                                        // Skip referenced targets that don't meet this configuration specs (eg. Editor target should skip Android platform)
                                        if (!referenceTarget.Platforms.Contains(configurationData.Platform))
                                            continue;
                                        if (!referenceTarget.Architectures.Contains(configurationData.Architecture))
                                            continue;

                                        try
                                        {
                                            var referenceBuildOptions = GetBuildOptions(referenceTarget, configurationData.TargetBuildOptions.Platform, configurationData.TargetBuildOptions.Toolchain, configurationData.Architecture, configurationData.Configuration, reference.Project.ProjectFolderPath);
                                            referenceBuildOptions.Flags |= BuildFlags.GenerateProject;
                                            var referenceModules = CollectModules(rules, referenceBuildOptions.Platform, referenceTarget, referenceBuildOptions, referenceBuildOptions.Toolchain, referenceBuildOptions.Architecture, referenceBuildOptions.Configuration);
                                            var referenceBinaryModules = referenceModules.Keys.GroupBy(x => x.BinaryModuleName).ToArray();
                                            foreach (var binaryModule in referenceBinaryModules)
                                            {
                                                var binaryModuleName = binaryModule.Key;
                                                if (string.IsNullOrEmpty(binaryModuleName))
                                                    continue;
                                                project.Defines.Add(binaryModuleName.ToUpperInvariant() + "_API=");
                                            }
                                        }
                                        catch
                                        {
                                            // Ignore exceptions
                                        }
                                    }
                                }
                            }

                            foreach (var binaryModule in binaryModules)
                            {
                                project.Defines.Add(binaryModule.Key.ToUpperInvariant() + "_API=");
                            }

                            // Skip using C++ project if any included binary module is C#-only
                            if (binaryModules.Any(x => x.Value.Any(y => y.BuildNativeCode)) && !rootProject.IsCSharpOnlyProject)
                            {
                                projects.Add(project);
                                if (mainSolutionProject == null && projectInfo == rootProject)
                                    mainSolutionProject = project;
                            }
                            else if (targets[0].CustomExternalProjectFilePath != null)
                                projects.Add(project);
                        }

                        // Create API bindings projects
                        foreach (var binaryModule in binaryModules)
                        {
                            var binaryModuleName = binaryModule.Key;

                            // Skip excluded modules with empty BinaryModuleName
                            if (string.IsNullOrEmpty(binaryModuleName))
                                continue;

                            // Skip bindings projects for prebuilt targets (eg. no sources to build/view - just binaries)
                            if (targets[0].IsPreBuilt)
                            {
                                Log.Verbose($"Skipping prebuilt module {binaryModuleName}");
                                continue;
                            }

                            // Skip if project of that name has been already added
                            if (projects.Any(x => x.OutputType == TargetOutputType.Library && x.Type == TargetType.DotNetCore && x.BaseName == binaryModuleName))
                            {
                                Log.Verbose($"Skipping already added module {binaryModuleName}");
                                continue;
                            }

                            using (new ProfileEventScope(binaryModuleName))
                            {
                                // TODO: add support for extending this code and support generating bindings projects for other scripting languages

                                // Get source files
                                var sourceFiles = new List<string>();
                                var generatedSourceFiles = new List<string>();
                                foreach (var module in binaryModule.Value)
                                {
                                    // C# sources included into build source files
                                    sourceFiles.AddRange(modulesBuildOptions[module].SourceFiles.Where(x => x.EndsWith(".cs")));
                                }
                                sourceFiles.RemoveAll(x => x.EndsWith(BuildFilesPostfix));
                                foreach (var target in targets)
                                {
                                    foreach (var configurationData in mainProject.Configurations)
                                    {
                                        if (target != configurationData.Target)
                                            continue;
                                        foreach (var q in configurationData.Modules)
                                        {
                                            if (!q.Key.FolderPath.StartsWith(target.FolderPath))
                                                continue;

                                            // Generated C# bindings source files (per-target, per-platform, per-configuration)
                                            var intermediateFolder = Path.Combine(q.Value.WorkingDirectory, Configuration.IntermediateFolder, target.Name, configurationData.PlatformName, configurationData.ArchitectureName, configurationData.ConfigurationName);
                                            var path = Path.Combine(intermediateFolder, q.Key.Name, q.Key.Name + ".Bindings.Gen.cs");
                                            if (!File.Exists(path))
                                            {
                                                Directory.CreateDirectory(Path.GetDirectoryName(path));
                                                File.WriteAllText(path, string.Empty, new UTF8Encoding());
                                            }
                                            generatedSourceFiles.Add(path);
                                        }
                                    }
                                }
                                sourceFiles.Add(Path.Combine(targets[0].FolderPath, binaryModuleName + ".Gen.cs"));
                                if (mainSolutionProject == null)
                                {
                                    // Add shaders sources to C# bindings project if not using C++ project
                                    var shadersSourcePath = Path.Combine(projectInfo.ProjectFolderPath, "Source/Shaders");
                                    if (Directory.Exists(shadersSourcePath))
                                        sourceFiles.AddRange(Directory.GetFiles(shadersSourcePath, "*", SearchOption.AllDirectories));
                                }
                                sourceFiles.Sort();

                                // Create project description
                                var project = dotNetProjectGenerator.CreateProject();
                                project.Type = TargetType.DotNetCore;
                                project.Name = project.BaseName = binaryModuleName;
                                if (projects.Any(x => x.Name == project.Name))
                                    project.Name += ".CSharp"; // Prevent overlapping name with native code project
                                project.OutputType = TargetOutputType.Library;
                                project.Targets = targets;
                                project.SearchPaths = Array.Empty<string>();
                                project.WorkspaceRootPath = mainProject.WorkspaceRootPath;
                                project.GroupName = mainProject.GroupName;
                                if (project.WorkspaceRootPath.StartsWith(workspaceRoot))
                                {
                                    // Place project targets binary modules bindings in the Source folder of the project (Visual Studio handles C# source files better in that way)
                                    project.Path = Path.Combine(project.WorkspaceRootPath, "Source", project.Name + '.' + dotNetProjectGenerator.ProjectFileExtension);
                                }
                                else
                                {
                                    // Default project files location
                                    project.Path = Path.Combine(projectsRoot, project.Name + '.' + dotNetProjectGenerator.ProjectFileExtension);
                                }
                                project.SourceFiles = sourceFiles;
                                project.GeneratedSourceFiles = generatedSourceFiles;
                                project.CSharp.UseFlaxVS = true;
                                project.Configurations.AddRange(mainProject.Configurations);
                                projectToBinaryModule.Add(project, binaryModule);
                                projectToModulesBuildOptions.Add(project, modulesBuildOptions);
                                projects.Add(project);
                                if (mainSolutionProject == null && projectInfo == rootProject)
                                    mainSolutionProject = project;
                            }
                        }
                    }
                }

                // Setup cross-project references
                using (new ProfileEventScope("ProjectReferences"))
                {
                    foreach (var project in projects)
                    {
                        if (!projectToBinaryModule.TryGetValue(project, out var binaryModule) ||
                            !projectToModulesBuildOptions.TryGetValue(project, out var modulesBuildOptions))
                            continue;

                        foreach (var module in binaryModule.Value)
                        {
                            if (!modulesBuildOptions.TryGetValue(module, out var moduleBuildOptions))
                                continue;

                            // Combine build options from this module
                            project.CSharp.SystemReferences.AddRange(moduleBuildOptions.ScriptingAPI.SystemReferences);
                            project.CSharp.FileReferences.AddRange(moduleBuildOptions.ScriptingAPI.FileReferences);
                            project.CSharp.NugetPackageReferences.AddRange(moduleBuildOptions.NugetPackageReferences);

                            // Find references based on the modules dependencies (external or from projects)
                            foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies))
                            {
                                var dependencyModule = rules.GetModule(dependencyName);
                                if (dependencyModule != null &&
                                    !string.IsNullOrEmpty(dependencyModule.BinaryModuleName) &&
                                    dependencyModule.BinaryModuleName != binaryModule.Key)
                                {
                                    var dependencyProject = projects.Find(x => x != null && x.Generator.Type == project.Generator.Type && x.BaseName == dependencyModule.BinaryModuleName);
                                    if (dependencyProject != null)
                                    {
                                        // Reference that project
                                        project.Dependencies.Add(dependencyProject);
                                    }
                                    else if (dependencyModule.BinaryModuleName == "FlaxEngine")
                                    {
                                        // TODO: instead of this hack find a way to reference the prebuilt target bindings binary (example: game C# project references FlaxEngine C# prebuilt dll)
                                        project.CSharp.FileReferences.Add(Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), "Development/FlaxEngine.CSharp.dll"));
                                    }
                                }
                            }
                        }
                    }
                }

                // When generating C#-only projects for Game that uses source-engine distribution replace dependencies on FlaxEngine with fixed dll file refs to fix Intellisense issues
                if (rootProject.IsCSharpOnlyProject)
                {
                    Project flaxDependencyToRemove = null;
                    foreach (var project in projects)
                    {
                        if (project.BaseName != "FlaxEngine")
                        {
                            var flaxDependency = project.Dependencies.FirstOrDefault(x => x.BaseName == "FlaxEngine");
                            if (flaxDependency != null)
                            {
                                project.Dependencies.Remove(flaxDependency);

                                // TODO: instead of this hack find a way to reference the prebuilt target bindings binary (example: game C# project references FlaxEngine C# prebuilt dll)
                                project.CSharp.FileReferences.Add(Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), "Development/FlaxEngine.CSharp.dll"));

                                // Remove FlaxEngine from projects to prevent duplicated types errors in Intellisense (eg. Actor type defined in both FlaxEngine.CSharp.dll and FlaxEngine.csproj)
                                flaxDependencyToRemove = flaxDependency;
                            }
                        }
                    }
                    if (flaxDependencyToRemove != null)
                    {
                        Log.Verbose($"Removing project reference {flaxDependencyToRemove.Name}");
                        projects.Remove(flaxDependencyToRemove);
                        foreach (var project in projects)
                            project.Dependencies.Remove(flaxDependencyToRemove);
                    }
                }

                // Setup custom projects
                GenerateCustomProjects?.Invoke(projects);
                nativeProjectGenerator.GenerateCustomProjects(projects);

                // Generate projects
                using (new ProfileEventScope("GenerateProjects"))
                {
                    foreach (var project in projects)
                    {
                        Log.Verbose($"Project {project.Name} -> {project.Path}");
                        project.Generate(solutionPath, project == mainSolutionProject);
                    }
                }

                // Generate C# project for build scripts files
                string rulesProjectName = "BuildScripts";
                using (new ProfileEventScope(rulesProjectName))
                {
                    // Create dummy target
                    var target = new Target
                    {
                        Name = rulesProjectName,
                        ProjectName = rulesProjectName,
                        FilePath = null,
                        FolderPath = null,
                        Type = TargetType.DotNetCore,
                        OutputType = TargetOutputType.Library,
                        Platforms = new[] { Platform.BuildPlatform.Target },
                        Configurations = new[] { TargetConfiguration.Debug },
                        Architectures = new[] { TargetArchitecture.AnyCPU },
                    };

                    // Create project description
                    Project project;
                    using (new ProfileEventScope("CreateProject"))
                    {
                        project = dotNetProjectGenerator.CreateProject();
                        project.Type = TargetType.DotNetCore;
                        project.Name = project.BaseName = rulesProjectName;
                        project.Targets = new[] { target };
                        project.SearchPaths = Array.Empty<string>();
                        project.WorkspaceRootPath = workspaceRoot;
                        project.Path = Path.Combine(projectsRoot, project.Name + '.' + dotNetProjectGenerator.ProjectFileExtension);
                        project.CSharp.OutputPath = Path.Combine(Environment.CurrentDirectory, "Cache", "Intermediate", "Unused");
                        project.SourceFiles = new List<string>();
                        foreach (var e in rules.Targets)
                            project.SourceFiles.Add(e.FilePath);
                        foreach (var e in rules.Modules)
                            project.SourceFiles.Add(e.FilePath);
                        project.CSharp.SystemReferences = new HashSet<string>
                        {
                            "System",
                            "System.Core",
                        };
                        SetupProjectConfigurations(project, rootProject);
                        if (project.Configurations.Count == 0)
                        {
                            // Hardcoded dummy configuration even if platform tools are missing for this platform
                            var platform = Platform.BuildPlatform;
                            var architecture = TargetArchitecture.x64;
                            var configuration = TargetConfiguration.Debug;
                            var buildOptions = GetBuildOptions(target, platform, null, architecture, configuration, project.WorkspaceRootPath);
                            buildOptions.Flags |= BuildFlags.GenerateProject;
                            project.Configurations.Add(new Project.ConfigurationData
                            {
                                Platform = platform.Target,
                                PlatformName = platform.Target.ToString(),
                                Architecture = architecture,
                                ArchitectureName = architecture.ToString(),
                                Configuration = configuration,
                                ConfigurationName = configuration.ToString(),
                                Target = target,
                                TargetBuildOptions = buildOptions,
                                Modules = new Dictionary<Module, BuildOptions>(),
                            });
                        }
                        var c = project.Configurations[0];
                        c.Name = "Debug|AnyCPU";
                        c.Text = "Debug";
                        project.Configurations[0] = c;

                        // Add reference to Flax.Build so rules can be validated in the IDE
                        project.CSharp.FileReferences.Add(Assembly.GetEntryAssembly().Location);
                    }

                    // Generate project
                    using (new ProfileEventScope("GenerateProject"))
                    {
                        Log.Verbose($"Project {rulesProjectName} -> {project.Path}");
                        dotNetProjectGenerator.GenerateProject(project, solutionPath, project == mainSolutionProject);
                    }

                    projects.Add(project);
                }

                // Generate solution
                using (new ProfileEventScope("Solution"))
                {
                    Solution solution;
                    using (new ProfileEventScope("CreateSolution"))
                    {
                        solution = nativeProjectGenerator.CreateSolution();
                        solution.Name = solutionName;
                        solution.WorkspaceRootPath = workspaceRoot;
                        solution.Path = solutionPath;
                        solution.Projects = projects.ToArray();
                        solution.MainProject = mainSolutionProject;
                    }

                    using (new ProfileEventScope("GenerateSolution"))
                    {
                        Log.Verbose("Solution -> " + solution.Path);
                        nativeProjectGenerator.GenerateSolution(solution);
                    }
                }
            }
        }
    }
}
