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

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

namespace Flax.Build.Projects.CMake
{
    /// <summary>
    /// Project generator for CMake-based IDE facade.
    /// </summary>
    public class CMakeProjectGenerator : ProjectGenerator
    {
        /// <inheritdoc />
        public override string ProjectFileExtension => "cmake";

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

        /// <inheritdoc />
        public override TargetType? Type => null;

        /// <inheritdoc />
        public override void GenerateProject(Project project, string solutionPath, bool isMainProject)
        {
            // Not used, solution contains all CMake project definitions.
        }

        /// <inheritdoc />
        public override void GenerateSolution(Solution solution)
        {
            var projectFolder = GetProjectFolder(solution);
            if (!Directory.Exists(projectFolder))
                Directory.CreateDirectory(projectFolder);

            var generateCLionFiles = Configuration.ProjectFormatCLion;
            var contents = new StringBuilder();
            var buildPresets = new List<CMakeBuildPreset>();
            var defaultBuildTarget = string.Empty;
            var targetNames = new HashSet<string>();
            var buildToolPath = Path.ChangeExtension(typeof(Builder).Assembly.Location, null) + Utilities.GetPlatformExecutableExt();

            contents.AppendLine("cmake_minimum_required(VERSION 3.20)");
            contents.AppendLine();
            contents.AppendLine("# Generated by Flax.Build. Do not edit manually.");
            contents.AppendLine("# CMake is used as an IDE facade. Actual builds are delegated to Flax.Build.");
            contents.AppendLine($"project({EscapeIdentifier(solution.Name)} LANGUAGES C CXX)");
            contents.AppendLine();
            contents.AppendLine("set(CMAKE_EXPORT_COMPILE_COMMANDS ON)");
            contents.AppendLine($"set(FLAX_WORKSPACE {QuotePath(solution.WorkspaceRootPath, solution.WorkspaceRootPath)})");
            contents.AppendLine($"set(FLAX_BUILD_TOOL {QuotePath(buildToolPath, solution.WorkspaceRootPath)})");
            if (DotNetSdk.Instance.IsValid)
                contents.AppendLine($"set(FLAX_DOTNET_ROOT {QuotePath(DotNetSdk.Instance.RootPath, solution.WorkspaceRootPath)})");
            contents.AppendLine();

            foreach (var project in solution.Projects.Where(x => x.Type == TargetType.NativeCpp))
            {
                var visibleFiles = GetProjectFiles(project, generateCLionFiles);
                var hasCodeModelConfiguration = TryGetCodeModelConfiguration(project, out var codeModelConfiguration);
                // Use Flax.Build-selected compile units for the code model. The wider project scan is only for visible files.
                var compileFiles = hasCodeModelConfiguration
                    ? GetCodeModelCompileFiles(codeModelConfiguration, generateCLionFiles)
                    : visibleFiles.Where(x => IsCompileSourceFile(x, generateCLionFiles)).ToArray();
                var files = new HashSet<string>(visibleFiles);
                files.AddRange(compileFiles);
                var allFiles = files.OrderBy(x => x).ToArray();
                var compileFileSet = new HashSet<string>(compileFiles);
                var nonCompileFiles = allFiles.Where(x => !compileFileSet.Contains(x)).ToArray();
                var includePaths = hasCodeModelConfiguration ? GetIncludePaths(project, codeModelConfiguration) : Array.Empty<string>();
                var definitions = hasCodeModelConfiguration ? GetPreprocessorDefinitions(project, codeModelConfiguration) : project.Defines.OrderBy(x => x).ToArray();
                var cppStandard = hasCodeModelConfiguration ? GetCppStandard(codeModelConfiguration) : 14;
                var ideTargetName = GetUniqueTargetName(targetNames, "IDE_" + project.Name);

                contents.AppendLine($"# {project.Name}");
                if (compileFiles.Length != 0)
                {
                    contents.AppendLine($"add_library({ideTargetName} STATIC EXCLUDE_FROM_ALL)");
                    contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES EXCLUDE_FROM_ALL TRUE)");
                    AppendTargetSources(contents, ideTargetName, allFiles, solution.WorkspaceRootPath);
                    if (nonCompileFiles.Length != 0)
                    {
                        contents.AppendLine("set_source_files_properties(");
                        foreach (var file in nonCompileFiles)
                            contents.AppendLine($"    {QuotePath(file, solution.WorkspaceRootPath)}");
                        contents.AppendLine("    PROPERTIES HEADER_FILE_ONLY TRUE");
                        contents.AppendLine(")");
                    }
                    contents.AppendLine($"set_target_properties({ideTargetName} PROPERTIES CXX_STANDARD {cppStandard} CXX_STANDARD_REQUIRED YES)");
                    AppendTargetPaths(contents, ideTargetName, "target_include_directories", "PRIVATE", includePaths, solution.WorkspaceRootPath);
                    AppendTargetValues(contents, ideTargetName, "target_compile_definitions", "PRIVATE", definitions);
                }
                else if (allFiles.Length != 0)
                {
                    contents.AppendLine($"add_custom_target({ideTargetName} SOURCES");
                    foreach (var file in allFiles)
                        contents.AppendLine($"    {QuotePath(file, solution.WorkspaceRootPath)}");
                    contents.AppendLine(")");
                }

                foreach (var configuration in project.Configurations)
                {
                    if (generateCLionFiles && !IsCLionDevelopmentPlatform(configuration.Platform))
                        continue;

                    var buildTargetName = GetUniqueTargetName(targetNames, "Build_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName);

                    AppendBuildTarget(contents, buildTargetName, "-build", configuration);
                    var presetSuffix = project.Name + "-" + configuration.Target.Name + "-" + configuration.PlatformName + "-" + configuration.ConfigurationName + "-" + configuration.ArchitectureName;
                    if (IsHostConfiguration(configuration))
                        AddBuildPreset(buildPresets, "build-" + presetSuffix, "Build " + presetSuffix, buildTargetName);

                    if (defaultBuildTarget.Length == 0 && project == solution.MainProject && configuration.Platform == Platform.BuildPlatform.Target && configuration.Architecture == Platform.BuildTargetArchitecture && configuration.Configuration == TargetConfiguration.Development)
                        defaultBuildTarget = buildTargetName;

                    if (generateCLionFiles)
                    {
                        var runProgram = GetRunProgram(project, configuration, out var runArguments);
                        if (IsHostConfiguration(configuration) && !string.IsNullOrEmpty(runProgram))
                        {
                            var runTargetName = GetUniqueTargetName(targetNames, "App_" + project.Name + "_" + configuration.Target.Name + "_" + configuration.PlatformName + "_" + configuration.ConfigurationName + "_" + configuration.ArchitectureName);
                            AppendRunTarget(contents, runTargetName, runProgram, runArguments);
                            contents.AppendLine($"add_dependencies({runTargetName} {buildTargetName})");
                            contents.AppendLine();
                        }
                    }
                }

                contents.AppendLine();
            }

            if (!string.IsNullOrEmpty(defaultBuildTarget))
            {
                contents.AppendLine($"add_custom_target({EscapeIdentifier(solution.Name)}_Build ALL DEPENDS {defaultBuildTarget})");
                contents.AppendLine();
            }

            Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakeLists.txt"), contents.ToString());
            GeneratePresets(solution, projectFolder, buildPresets);
        }

        /// <summary>
        /// Gets the directory that contains the generated CMake facade project.
        /// </summary>
        public static string GetProjectFolder(Solution solution)
        {
            return Path.Combine(solution.WorkspaceRootPath, "Cache", "Projects", "CMake", solution.Name);
        }

        private struct CMakeBuildPreset
        {
            public string Name;
            public string DisplayName;
            public string Target;
        }

        private static void AddBuildPreset(List<CMakeBuildPreset> presets, string name, string displayName, string target)
        {
            presets.Add(new CMakeBuildPreset
            {
                Name = EscapePresetName(name),
                DisplayName = displayName,
                Target = target,
            });
        }

        private static void GeneratePresets(Solution solution, string projectFolder, List<CMakeBuildPreset> buildPresets)
        {
            var contents = new StringBuilder();
            contents.AppendLine("{");
            contents.AppendLine("  \"version\": 3,");
            contents.AppendLine("  \"cmakeMinimumRequired\": {");
            contents.AppendLine("    \"major\": 3,");
            contents.AppendLine("    \"minor\": 20,");
            contents.AppendLine("    \"patch\": 0");
            contents.AppendLine("  },");
            contents.AppendLine("  \"configurePresets\": [");
            contents.AppendLine("    {");
            contents.AppendLine("      \"name\": \"flax\",");
            contents.AppendLine("      \"displayName\": \"Flax\",");
            contents.AppendLine("      \"description\": \"Configure Flax CMake facade for IDE integration.\",");
            contents.AppendLine("      \"generator\": \"Ninja\",");
            contents.AppendLine("      \"binaryDir\": \"${sourceDir}/cmake-build-flax\",");
            contents.AppendLine("      \"cacheVariables\": {");
            contents.AppendLine("        \"CMAKE_BUILD_TYPE\": \"Debug\",");
            contents.AppendLine("        \"CMAKE_EXPORT_COMPILE_COMMANDS\": \"ON\"");
            contents.AppendLine("      }");
            contents.AppendLine("    }");
            contents.AppendLine("  ],");
            contents.AppendLine("  \"buildPresets\": [");
            for (var i = 0; i < buildPresets.Count; i++)
            {
                var preset = buildPresets[i];
                contents.AppendLine("    {");
                contents.AppendLine($"      \"name\": {JsonString(preset.Name)},");
                contents.AppendLine($"      \"displayName\": {JsonString(preset.DisplayName)},");
                contents.AppendLine("      \"configurePreset\": \"flax\",");
                contents.AppendLine($"      \"targets\": [{JsonString(preset.Target)}]");
                contents.Append("    }");
                if (i != buildPresets.Count - 1)
                    contents.Append(',');
                contents.AppendLine();
            }
            contents.AppendLine("  ]");
            contents.AppendLine("}");

            Utilities.WriteFileIfChanged(Path.Combine(projectFolder, "CMakePresets.json"), contents.ToString());
        }

        private static void AppendBuildTarget(StringBuilder contents, string targetName, string action, Project.ConfigurationData configuration)
        {
            contents.AppendLine($"add_custom_target({targetName}");
            contents.Append("    COMMAND ");
            if (DotNetSdk.Instance.IsValid)
                contents.Append("${CMAKE_COMMAND} -E env \"DOTNET_ROOT=${FLAX_DOTNET_ROOT}\" ");
            contents.AppendLine($"\"${{FLAX_BUILD_TOOL}}\" {Quote(action)} \"-log\" \"-mutex\"");
            contents.AppendLine("        \"-workspace=${FLAX_WORKSPACE}\"");
            contents.AppendLine($"        \"-arch={configuration.ArchitectureName}\"");
            contents.AppendLine($"        \"-configuration={configuration.ConfigurationName}\"");
            contents.AppendLine($"        \"-platform={configuration.PlatformName}\"");
            contents.AppendLine($"        \"-buildTargets={configuration.Target.Name}\"");
            if (!string.IsNullOrEmpty(Configuration.Compiler))
                contents.AppendLine($"        \"-compiler={EscapeString(Configuration.Compiler)}\"");
            if (!string.IsNullOrEmpty(Configuration.Dotnet))
                contents.AppendLine($"        \"-dotnet={EscapeString(Configuration.Dotnet)}\"");
            if (Configuration.Sanitizers != Sanitizer.None)
                contents.AppendLine($"        \"-sanitizers={Configuration.Sanitizers}\"");
            contents.AppendLine("    WORKING_DIRECTORY \"${FLAX_WORKSPACE}\"");
            contents.AppendLine("    USES_TERMINAL");
            contents.AppendLine("    VERBATIM");
            contents.AppendLine(")");
            contents.AppendLine();
        }

        private static void AppendRunTarget(StringBuilder contents, string targetName, string program, string[] arguments)
        {
            var sourcePath = $"${{CMAKE_CURRENT_BINARY_DIR}}/{targetName}.cpp";
            contents.AppendLine($"file(WRITE \"{sourcePath}\" [=[");
            contents.AppendLine("#include <cstdio>");
            contents.AppendLine("#include <vector>");
            contents.AppendLine("#ifdef _WIN32");
            contents.AppendLine("#include <process.h>");
            contents.AppendLine("#else");
            contents.AppendLine("#include <unistd.h>");
            contents.AppendLine("#endif");
            contents.AppendLine("int main(int argc, char** argv) {");
            contents.AppendLine($"    const char* program = \"{CppString(program)}\";");
            contents.AppendLine("    std::vector<const char*> args;");
            contents.AppendLine("    args.reserve(argc + 1);");
            contents.AppendLine("    args.push_back(program);");
            foreach (var arg in arguments)
                contents.AppendLine($"    args.push_back(\"{CppString(arg)}\");");
            contents.AppendLine("    for (int i = 1; i < argc; i++)");
            contents.AppendLine("        args.push_back(argv[i]);");
            contents.AppendLine("    args.push_back(nullptr);");
            contents.AppendLine("#ifdef _WIN32");
            contents.AppendLine("    _execv(args[0], args.data());");
            contents.AppendLine("#else");
            contents.AppendLine("    execv(args[0], const_cast<char* const*>(args.data()));");
            contents.AppendLine("#endif");
            contents.AppendLine("    perror(args[0]);");
            contents.AppendLine("    return 1;");
            contents.AppendLine("}");
            contents.AppendLine("]=])");
            contents.AppendLine($"add_executable({targetName} EXCLUDE_FROM_ALL \"{sourcePath}\")");
            contents.AppendLine($"set_target_properties({targetName} PROPERTIES");
            contents.AppendLine("    RUNTIME_OUTPUT_DIRECTORY \"${CMAKE_CURRENT_BINARY_DIR}/FlaxLaunchers\"");
            contents.AppendLine(")");
        }

        private static void AppendTargetSources(StringBuilder contents, string targetName, string[] files, string workspaceRoot)
        {
            contents.AppendLine($"target_sources({targetName} PRIVATE");
            foreach (var file in files)
                contents.AppendLine($"    {QuotePath(file, workspaceRoot)}");
            contents.AppendLine(")");
        }

        private static void AppendTargetPaths(StringBuilder contents, string targetName, string command, string visibility, string[] values, string workspaceRoot)
        {
            if (values.Length == 0)
                return;

            contents.AppendLine($"{command}({targetName} {visibility}");
            foreach (var value in values)
                contents.AppendLine($"    {QuotePath(value, workspaceRoot)}");
            contents.AppendLine(")");
        }

        private static void AppendTargetValues(StringBuilder contents, string targetName, string command, string visibility, string[] values)
        {
            if (values.Length == 0)
                return;

            contents.AppendLine($"{command}({targetName} {visibility}");
            foreach (var value in values)
                contents.AppendLine($"    {Quote(value)}");
            contents.AppendLine(")");
        }

        private static string[] GetProjectFiles(Project project, bool generateCLionFiles)
        {
            var files = new HashSet<string>();
            if (project.SourceFiles != null)
                files.AddRange(project.SourceFiles);
            if (project.GeneratedSourceFiles != null)
                files.AddRange(project.GeneratedSourceFiles);
            if (project.SourceDirectories != null)
            {
                foreach (var folder in project.SourceDirectories)
                {
                    if (Directory.Exists(folder))
                        files.AddRange(Directory.GetFiles(folder, "*", SearchOption.AllDirectories));
                }
            }
            files.RemoveWhere(x =>
                !IsProjectFile(x) ||
                Path.GetFileName(x).Equals(".DS_Store", StringComparison.OrdinalIgnoreCase) ||
                (generateCLionFiles && !IsCLionDevelopmentSourceFile(x)));
            return files.OrderBy(x => x).ToArray();
        }

        private static string[] GetCodeModelCompileFiles(Project.ConfigurationData configuration, bool generateCLionFiles)
        {
            var files = new HashSet<string>();
            var definitions = new HashSet<string>(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions);
            AddCompileFiles(files, configuration.TargetBuildOptions.SourceFiles, generateCLionFiles, definitions);
            if (configuration.Modules != null)
            {
                foreach (var module in configuration.Modules)
                {
                    if (module.Key.BuildNativeCode)
                        AddCompileFiles(files, module.Value.SourceFiles, generateCLionFiles, definitions);
                }
            }
            return files.OrderBy(x => x).ToArray();
        }

        private static void AddCompileFiles(HashSet<string> files, IEnumerable<string> sourceFiles, bool generateCLionFiles, HashSet<string> definitions = null)
        {
            if (sourceFiles == null)
                return;

            foreach (var file in sourceFiles)
            {
                if (IsCompileSourceFile(file, generateCLionFiles, definitions))
                    files.Add(file);
            }
        }

        private static bool TryGetCodeModelConfiguration(Project project, out Project.ConfigurationData configuration)
        {
            foreach (var e in project.Configurations)
            {
                if (e.TargetBuildOptions != null &&
                    e.Platform == Platform.BuildPlatform.Target &&
                    e.Architecture == Platform.BuildTargetArchitecture &&
                    e.Configuration == TargetConfiguration.Development)
                {
                    configuration = e;
                    return true;
                }
            }

            foreach (var e in project.Configurations)
            {
                if (e.TargetBuildOptions != null)
                {
                    configuration = e;
                    return true;
                }
            }

            configuration = default;
            return false;
        }

        private static bool IsHostConfiguration(Project.ConfigurationData configuration)
        {
            return configuration.Platform == Platform.BuildPlatform.Target &&
                   configuration.Architecture == Platform.BuildTargetArchitecture;
        }

        private static bool IsCLionDevelopmentPlatform(TargetPlatform platform)
        {
            switch (platform)
            {
            case TargetPlatform.Windows:
            case TargetPlatform.Linux:
            case TargetPlatform.Mac:
                return true;
            default:
                return false;
            }
        }

        private static string[] GetIncludePaths(Project project, Project.ConfigurationData configuration)
        {
            var result = new HashSet<string>();
            if (project.SearchPaths != null)
                result.AddRange(project.SearchPaths);
            result.AddRange(configuration.TargetBuildOptions.CompileEnv.IncludePaths);
            return result.OrderBy(x => x).ToArray();
        }

        private static string[] GetPreprocessorDefinitions(Project project, Project.ConfigurationData configuration)
        {
            var result = new HashSet<string>();
            result.AddRange(project.Defines);
            result.AddRange(configuration.TargetBuildOptions.CompileEnv.PreprocessorDefinitions);
            return result.OrderBy(x => x).ToArray();
        }

        private static int GetCppStandard(Project.ConfigurationData configuration)
        {
            var result = 14;
            switch (configuration.TargetBuildOptions.CompileEnv.CppVersion)
            {
            case CppVersion.Cpp17:
                result = Math.Max(result, 17);
                break;
            case CppVersion.Cpp20:
            case CppVersion.Latest:
                result = Math.Max(result, 20);
                break;
            }
            return result;
        }

        private static string GetRunProgram(Project project, Project.ConfigurationData configuration, out string[] arguments)
        {
            var target = configuration.Target;
            var outputType = project.OutputType ?? target.OutputType;
            if (ShouldRunEditorForProject(configuration, outputType))
            {
                var program = Path.Combine(Globals.EngineRoot, Platform.GetEditorBinaryDirectory(), configuration.ConfigurationName, "FlaxEditor" + Utilities.GetPlatformExecutableExt());
                var args = new List<string>();
                if (configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac)
                    args.Add("-std");
                args.Add("-project");
                args.Add(project.WorkspaceRootPath);
                args.Add("-skipCompile");
                arguments = args.ToArray();
                return program;
            }

            arguments = configuration.Platform == TargetPlatform.Linux || configuration.Platform == TargetPlatform.Mac ? new[] { "--std" } : Array.Empty<string>();
            return target.GetOutputFilePath(configuration.TargetBuildOptions, TargetOutputType.Executable);
        }

        private static bool ShouldRunEditorForProject(Project.ConfigurationData configuration, TargetOutputType outputType)
        {
            return outputType != TargetOutputType.Executable && configuration.Target.IsEditor;
        }

        private static bool IsCppCompileFile(string path)
        {
            var extension = Path.GetExtension(path);
            return extension.Equals(".c", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".cc", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".cpp", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".cxx", StringComparison.OrdinalIgnoreCase);
        }

        private static bool IsCompileSourceFile(string path, bool generateCLionFiles, HashSet<string> definitions = null)
        {
            return IsCppCompileFile(path) &&
                   (!generateCLionFiles || (IsCLionDevelopmentSourceFile(path) && IsCLionSourceGuardActive(path, definitions)));
        }

        private static bool IsCLionDevelopmentSourceFile(string path)
        {
            path = Utilities.NormalizePath(path);
            return !ContainsPathSegment(path, "Android") &&
                   !ContainsPathSegment(path, "AndroidJNI") &&
                   !ContainsPathSegment(path, "GDK") &&
                   !ContainsPathSegment(path, "iOS") &&
                   !ContainsPathSegment(path, "PS4") &&
                   !ContainsPathSegment(path, "PS5") &&
                   !ContainsPathSegment(path, "Switch") &&
                   !ContainsPathSegment(path, "UWP") &&
                   !ContainsPathSegment(path, "Web") &&
                   !ContainsPathSegment(path, "XboxOne") &&
                   !ContainsPathSegment(path, "XboxScarlett") &&
                   !Path.GetFileName(path).Contains(".Web.", StringComparison.OrdinalIgnoreCase);
        }

        private static bool ContainsPathSegment(string path, string segment)
        {
            return path.IndexOf("/" + segment + "/", StringComparison.OrdinalIgnoreCase) != -1;
        }

        private static bool IsCLionSourceGuardActive(string path, HashSet<string> definitions)
        {
            if (definitions == null)
                return true;

            path = Utilities.NormalizePath(path);
            if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Windows"))
                return HasDefinition(definitions, "PLATFORM_TOOLS_WINDOWS");
            if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Linux"))
                return HasDefinition(definitions, "PLATFORM_TOOLS_LINUX");
            if (ContainsSourcePath(path, "Source/Editor/Cooker/Platform/Mac"))
                return HasDefinition(definitions, "PLATFORM_TOOLS_MAC");

            if (ContainsSourcePath(path, "Source/Engine/Engine/Windows"))
                return HasDefinition(definitions, "PLATFORM_WINDOWS") && !HasDefinition(definitions, "USE_EDITOR");
            if (ContainsSourcePath(path, "Source/Engine/Engine/Linux"))
                return HasDefinition(definitions, "PLATFORM_LINUX") && !HasDefinition(definitions, "USE_EDITOR");
            if (ContainsSourcePath(path, "Source/Engine/Engine/Mac"))
                return HasDefinition(definitions, "PLATFORM_MAC") && !HasDefinition(definitions, "USE_EDITOR");

            if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Win32"))
                return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && (HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS"));
            if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Linux"))
                return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_LINUX");
            if (ContainsSourcePath(path, "Source/Engine/GraphicsDevice/Vulkan/Mac"))
                return HasDefinition(definitions, "GRAPHICS_API_VULKAN") && HasDefinition(definitions, "PLATFORM_MAC");

            if (ContainsSourcePath(path, "Source/Engine/Platform/Win32"))
                return HasDefinition(definitions, "PLATFORM_WIN32") || HasDefinition(definitions, "PLATFORM_WINDOWS");
            if (ContainsSourcePath(path, "Source/Engine/Platform/Windows"))
                return HasDefinition(definitions, "PLATFORM_WINDOWS");
            if (ContainsSourcePath(path, "Source/Engine/Platform/Linux"))
                return HasDefinition(definitions, "PLATFORM_LINUX");
            if (ContainsSourcePath(path, "Source/Engine/Platform/Mac"))
                return HasDefinition(definitions, "PLATFORM_MAC");
            if (ContainsSourcePath(path, "Source/Engine/Platform/Apple"))
                return HasDefinition(definitions, "PLATFORM_MAC") || HasDefinition(definitions, "PLATFORM_IOS");
            if (ContainsSourcePath(path, "Source/Engine/Platform/Unix"))
                return HasDefinition(definitions, "PLATFORM_UNIX");

            return true;
        }

        private static bool ContainsSourcePath(string path, string sourcePath)
        {
            return path.IndexOf("/" + sourcePath + "/", StringComparison.OrdinalIgnoreCase) != -1;
        }

        private static bool HasDefinition(HashSet<string> definitions, string name)
        {
            return definitions.Contains(name) ||
                   definitions.Any(x => x.StartsWith(name + "=", StringComparison.Ordinal));
        }

        private static bool IsProjectFile(string path)
        {
            var extension = Path.GetExtension(path);
            return IsCppCompileFile(path) ||
                   extension.Equals(".h", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".hh", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".hpp", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".hxx", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".inl", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".rc", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".manifest", StringComparison.OrdinalIgnoreCase) ||
                   extension.Equals(".natvis", StringComparison.OrdinalIgnoreCase);
        }

        private static string GetUniqueTargetName(HashSet<string> names, string name)
        {
            var result = EscapeIdentifier(name);
            var baseName = result;
            var index = 2;
            while (!names.Add(result))
                result = baseName + "_" + index++;
            return result;
        }

        private static string EscapeIdentifier(string value)
        {
            if (string.IsNullOrEmpty(value))
                return "_";

            var result = new StringBuilder(value.Length);
            foreach (var c in value)
                result.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_');
            if (char.IsDigit(result[0]))
                result.Insert(0, '_');
            return result.ToString();
        }

        private static string EscapePresetName(string value)
        {
            var result = new StringBuilder(value.Length);
            foreach (var c in value)
                result.Append(char.IsLetterOrDigit(c) || c == '-' || c == '_' || c == '.' ? char.ToLowerInvariant(c) : '-');
            return result.ToString();
        }

        private static string QuotePath(string value, string workspaceRoot, bool includeQuotes = true)
        {
            value = Utilities.NormalizePath(value);
            workspaceRoot = Utilities.NormalizePath(workspaceRoot).TrimEnd('/');
            if (value.StartsWith(workspaceRoot + "/", StringComparison.Ordinal))
                value = "${FLAX_WORKSPACE}/" + value.Substring(workspaceRoot.Length + 1);
            return Quote(value, includeQuotes);
        }

        private static string Quote(string value, bool includeQuotes = true)
        {
            value = EscapeString(value);
            return includeQuotes ? "\"" + value + "\"" : value;
        }

        private static string EscapeString(string value)
        {
            return value.Replace("\\", "\\\\").Replace(";", "\\;").Replace("\"", "\\\"");
        }

        private static string JsonString(string value)
        {
            return "\"" + value.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
        }

        private static string CppString(string value)
        {
            return value.Replace("\\", "\\\\").Replace("\"", "\\\"").Replace("\n", "\\n").Replace("\r", "\\r");
        }
    }
}
