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

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

namespace Flax.Deps.Dependencies
{
    /// <summary>
    /// NVIDIA PhysX SDK
    /// </summary>
    /// <seealso cref="Flax.Deps.Dependency" />
    class PhysX : Dependency
    {
        private string root;
        private string projectGenDir;
        private string projectGenPath;
        private string solutionFilesRoot;

        private static void ConfigureCmakeSwitch(XmlElement cmakeSwitches, string name, string value)
        {
            if (cmakeSwitches == null)
                return;
            foreach (XmlElement cmakeSwitch in cmakeSwitches.ChildNodes)
            {
                if (cmakeSwitch.HasAttribute("name") && cmakeSwitch.Attributes["name"].Value == name)
                {
                    cmakeSwitch.Attributes["value"].Value = value;
                    return;
                }
            }
            var child = cmakeSwitches.OwnerDocument.CreateElement(cmakeSwitches.ChildNodes[0].Name);
            child.SetAttribute("name", name);
            child.SetAttribute("value", value);
            cmakeSwitches.AppendChild(child);
        }

        private void Build(BuildOptions options, string preset, TargetPlatform targetPlatform, TargetArchitecture architecture)
        {
            // Load preset configuration
            var presetPath = Path.Combine(projectGenDir, "buildtools", "presets", "public", preset + ".xml");
            if (!File.Exists(presetPath))
                throw new Exception(string.Format("Missing PhysX preset {0} (file: {1})", preset, presetPath));
            var presetXml = new XmlDocument();
            presetXml.Load(presetPath);
            var usePVD = false;

            // Configure preset
            var cmakeSwitches = presetXml["preset"]["CMakeSwitches"];
            ConfigureCmakeSwitch(cmakeSwitches, "PX_BUILDSNIPPETS", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "PX_BUILDPVDRUNTIME", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "PX_BUILDSAMPLES", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "PX_BUILDPUBLICSAMPLES", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "PX_GENERATE_STATIC_LIBRARIES", "True");
            ConfigureCmakeSwitch(cmakeSwitches, "NV_USE_STATIC_WINCRT", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "NV_USE_DEBUG_WINCRT", "False");
            ConfigureCmakeSwitch(cmakeSwitches, "PX_FLOAT_POINT_PRECISE_MATH", "False");
            if (usePVD)
            {
                // PVD depends on metadata and serialization code striped from shipping builds
                ConfigureCmakeSwitch(cmakeSwitches, "PX_SERIALIZATION", "True");
            }
            var cmakeParams = presetXml["preset"]["CMakeParams"];
            switch (targetPlatform)
            {
            case TargetPlatform.Windows:
                if (architecture == TargetArchitecture.ARM64)
                {
                    // Windows ARM64 doesn't have precompiled files for GPU support, so avoid copying those DLLs around
                    ConfigureCmakeSwitch(cmakeSwitches, "PX_COPY_EXTERNAL_DLL", "OFF");
                    ConfigureCmakeSwitch(cmakeParams, "PX_COPY_EXTERNAL_DLL", "OFF");
                }
                break;
            case TargetPlatform.Linux:
                ConfigureCmakeSwitch(cmakeParams, "PHYSX_CXX_FLAGS", "\"-Wno-error=format -Wno-error=unused-but-set-variable -Wno-error=switch-default -Wno-error=invalid-offsetof -Wno-error=unsafe-buffer-usage -Wno-error=unsafe-buffer-usage-in-libc-call -Wno-error=missing-include-dirs\"");
                break;
            case TargetPlatform.Android:
                ConfigureCmakeSwitch(cmakeParams, "CMAKE_INSTALL_PREFIX", $"install/android-{AndroidConfiguration.PlatformApi}/PhysX");
                ConfigureCmakeSwitch(cmakeParams, "ANDROID_NATIVE_API_LEVEL", $"android-{AndroidConfiguration.PlatformApi}");
                ConfigureCmakeSwitch(cmakeParams, "ANDROID_ABI", AndroidToolchain.GetAbiName(architecture));
                break;
            case TargetPlatform.Mac:
                ConfigureCmakeSwitch(cmakeParams, "CMAKE_OSX_DEPLOYMENT_TARGET", MacConfiguration.MacOSXMinVer);
                ConfigureCmakeSwitch(cmakeParams, "PHYSX_CXX_FLAGS", "\"-Wno-error=format -Wno-error=unused-but-set-variable -Wno-error=switch-default -Wno-error=invalid-offsetof -Wno-error=unsafe-buffer-usage -Wno-error=unsafe-buffer-usage-in-libc-call -Wno-error=missing-include-dirs\"");
                break;
            case TargetPlatform.iOS:
                ConfigureCmakeSwitch(cmakeParams, "CMAKE_OSX_DEPLOYMENT_TARGET", iOSConfiguration.MinVer);
                ConfigureCmakeSwitch(cmakeParams, "CMAKE_XCODE_ATTRIBUTE_IPHONEOS_DEPLOYMENT_TARGET", iOSConfiguration.MinVer);
                break;
            case TargetPlatform.Web:
                ConfigureCmakeSwitch(cmakeParams, "PHYSX_CXX_FLAGS", WebConfiguration.Threads ? "\"-pthread\"" : "");
                break;
            }

            // Save preset
            presetXml.Save(presetPath);

            // Peek options
            var platform = Platform.GetPlatform(targetPlatform);
            var configuration = "release";
            string bits;
            string arch;
            string binariesSubDir;
            string buildPlatform = architecture == TargetArchitecture.x86 ? "Win32" : architecture.ToString();
            bool suppressBitsPostfix = false;
            string binariesPrefix = string.Empty;
            var envVars = new Dictionary<string, string>();
            envVars.Add("CMAKE_BUILD_PARALLEL_LEVEL", CmakeBuildParallel);
            switch (architecture)
            {
            case TargetArchitecture.x86:
                arch = "x86";
                bits = "32";
                break;
            case TargetArchitecture.x64:
                arch = "x86";
                bits = "64";
                break;
            case TargetArchitecture.ARM:
                arch = "arm";
                bits = "32";
                break;
            case TargetArchitecture.ARM64:
                arch = "arm";
                bits = "64";
                break;
            default: throw new InvalidArchitectureException(architecture);
            }
            var msBuildProps = new Dictionary<string, string>();
            switch (targetPlatform)
            {
            case TargetPlatform.Windows:
                binariesSubDir = string.Format("win.{0}_{1}.vc143.md", arch, bits);
                break;
            case TargetPlatform.Linux:
                binariesSubDir = "linux.clang";
                binariesPrefix = "lib";
                break;
            case TargetPlatform.PS4:
                binariesSubDir = "ps4";
                buildPlatform = "ORBIS";
                suppressBitsPostfix = true;
                binariesPrefix = "lib";
                break;
            case TargetPlatform.PS5:
                binariesSubDir = "ps5";
                buildPlatform = "PROSPERO";
                suppressBitsPostfix = true;
                binariesPrefix = "lib";
                break;
            case TargetPlatform.XboxOne:
            case TargetPlatform.XboxScarlett:
                binariesSubDir = "win.x86_64.vc142.md";
                break;
            case TargetPlatform.Android:
                switch (architecture)
                {
                case TargetArchitecture.ARM64:
                    binariesSubDir = "android.arm64-v8a";
                    break;
                default: throw new InvalidArchitectureException(architecture);
                }
                binariesPrefix = "lib";
                break;
            case TargetPlatform.Switch:
                binariesSubDir = "switch64";
                buildPlatform = "NX64";
                suppressBitsPostfix = true;
                binariesPrefix = "lib";
                envVars.Add("NintendoSdkRoot", Sdk.Get("SwitchSdk").RootPath + '\\');
                msBuildProps.Add("NintendoSdkRoot", envVars["NintendoSdkRoot"]);
                break;
            case TargetPlatform.Mac:
                switch (architecture)
                {
                case TargetArchitecture.x64:
                    binariesSubDir = "mac.x86_64";
                    break;
                case TargetArchitecture.ARM64:
                    binariesSubDir = "mac.arm64";
                    break;
                default: throw new InvalidArchitectureException(architecture);
                }
                binariesPrefix = "lib";
                envVars.Add("MACOSX_DEPLOYMENT_TARGET", MacConfiguration.MacOSXMinVer);
                break;
            case TargetPlatform.iOS:
                binariesSubDir = "ios.arm_64";
                binariesPrefix = "lib";
                envVars.Add("IPHONEOS_DEPLOYMENT_TARGET", iOSConfiguration.MinVer);
                break;
            case TargetPlatform.Web:
                binariesSubDir = "web32";
                binariesPrefix = "lib";
                envVars.Add("CMAKE_TOOLCHAIN_FILE", EmscriptenSdk.Instance.CMakeToolchainPath);
                break;
            default: throw new InvalidPlatformException(targetPlatform);
            }

            // Setup build environment variables for PhysX build system
            string msBuild = null;
            switch (BuildPlatform)
            {
            case TargetPlatform.Windows:
            {
                GetMsBuildForPlatform(targetPlatform, out var vsVersion, out msBuild);
                // TODO: override VS version in cmake_generate_projects.py too
                if (File.Exists(msBuild))
                {
                    envVars.Add("PATH", Path.GetDirectoryName(msBuild));
                }
                break;
            }
            case TargetPlatform.Linux:
                envVars.Add("CC", "clang-" + LinuxConfiguration.ClangMinVer);
                envVars.Add("CC_FOR_BUILD", "clang-" + LinuxConfiguration.ClangMinVer);
                envVars.Add("CXX", "clang++-" + LinuxConfiguration.ClangMinVer);
                break;
            case TargetPlatform.Mac: break;
            default: throw new InvalidPlatformException(BuildPlatform);
            }
            if (AndroidNdk.Instance.IsValid)
            {
                envVars.Add("ANDROID_NDK_HOME", AndroidNdk.Instance.RootPath);
                envVars.Add("PM_ANDROIDNDK_PATH", AndroidNdk.Instance.RootPath);
            }

            // Update packman for old PhysX version (https://github.com/NVIDIA-Omniverse/PhysX/issues/229)
            if (BuildPlatform == TargetPlatform.Windows)
                Utilities.Run(Path.Combine(projectGenDir, "buildtools", "packman", "packman.cmd"), "update -y");
            else
                Utilities.Run(Path.Combine(projectGenDir, "buildtools", "packman", "packman"), "update -y");

            // Print the PhysX version
            Log.Info("Building PhysX version " + File.ReadAllText(Path.Combine(root, "physx", "version.txt")) + " to " + binariesSubDir);

            // Generate project files
            Utilities.Run(projectGenPath, preset, null, projectGenDir, Utilities.RunOptions.DefaultTool, envVars);

            switch (targetPlatform)
            {
            case TargetPlatform.PS4:
            case TargetPlatform.PS5:
            case TargetPlatform.Switch:
                // Hack: Platform compiler uses .o extension for compiler output files but CMake uses .obj even if CMAKE_CXX_OUTPUT_EXTENSION/CMAKE_C_OUTPUT_EXTENSION are specified
                Utilities.ReplaceInFiles(Path.Combine(root, "physx\\compiler\\" + binariesSubDir), "*.vcxproj", SearchOption.AllDirectories, ".obj", ".o");
                break;
            case TargetPlatform.XboxOne:
            case TargetPlatform.XboxScarlett:
                // Hack: force to use proper Win10 SDK
                Utilities.ReplaceInFiles(Path.Combine(root, "physx\\compiler", preset), "*.vcxproj", SearchOption.AllDirectories, "10.0.18362.0", "10.0.19041.0");
                break;
            }

            // Run building based on the platform
            var defaultPhysXLibs = new[]
            {
                "PhysX",
                "PhysXCharacterKinematic",
                "PhysXCommon",
                "PhysXCooking",
                "PhysXExtensions",
                "PhysXFoundation",
                "PhysXPvdSDK",
                "PhysXVehicle",
                "PhysXVehicle2",
            };
            var dstBinaries = GetThirdPartyFolder(options, targetPlatform, architecture);
            var srcBinaries = Path.Combine(root, "physx", "bin", binariesSubDir, configuration);
            if (targetPlatform == TargetPlatform.Web)
                srcBinaries = Path.Combine(root, "physx", "compiler", binariesSubDir, "sdk_source_bin");
            switch (BuildPlatform)
            {
            case TargetPlatform.Windows:
                switch (targetPlatform)
                {
                case TargetPlatform.Android:
                    Utilities.Run("cmake", "--build .", null, Path.Combine(root, "physx\\compiler\\android-" + configuration), Utilities.RunOptions.ConsoleLogOutput, envVars);
                    break;
                case TargetPlatform.Web:
                    Utilities.Run("cmake", "--build .", null, Path.Combine(root, "physx\\compiler\\" + preset), Utilities.RunOptions.DefaultTool, envVars);
                    break;
                default:
                    VCEnvironment.BuildSolution(Path.Combine(solutionFilesRoot, preset, "PhysXSDK.sln"), configuration, buildPlatform, msBuildProps, msBuild);
                    break;
                }
                break;
            case TargetPlatform.Linux:
                Utilities.Run("make", null, null, Path.Combine(projectGenDir, "compiler", "linux-" + configuration), Utilities.RunOptions.ConsoleLogOutput, envVars);
                break;
            case TargetPlatform.Mac:
                Utilities.Run("xcodebuild", "-project PhysXSDK.xcodeproj -alltargets -configuration " + configuration, null, Path.Combine(projectGenDir, "compiler", preset), Utilities.RunOptions.ConsoleLogOutput, envVars);
                break;
            default: throw new InvalidPlatformException(BuildPlatform);
            }

            // Deploy binaries
            var binariesExtension = platform.StaticLibraryFileExtension;
            Log.Verbose("Copy PhysX binaries from " + srcBinaries);
            foreach (var physXLib in defaultPhysXLibs)
            {
                var remove = !usePVD && physXLib.Contains("Pvd");
                var filename = suppressBitsPostfix ? string.Format("{0}{1}_static", binariesPrefix, physXLib) : string.Format("{0}{1}_static_{2}", binariesPrefix, physXLib, bits);
                if (targetPlatform == TargetPlatform.Web)
                    filename = binariesPrefix + physXLib;
                filename += binariesExtension;
                if (remove)
                    Utilities.FileDelete(Path.Combine(dstBinaries, filename));
                else
                    Utilities.FileCopy(Path.Combine(srcBinaries, filename), Path.Combine(dstBinaries, filename));

                var filenamePdb = Path.ChangeExtension(filename, "pdb");
                if (remove)
                    Utilities.FileDelete(Path.Combine(dstBinaries, filenamePdb));
                else if (File.Exists(Path.Combine(srcBinaries, filenamePdb)))
                    Utilities.FileCopy(Path.Combine(srcBinaries, filenamePdb), Path.Combine(dstBinaries, filenamePdb));

                // Strip debug symbols to reduce binaries size
                switch (BuildPlatform)
                {
                case TargetPlatform.Linux:
                case TargetPlatform.Mac:
                    switch (targetPlatform)
                    {
                    case TargetPlatform.Mac:
                    case TargetPlatform.Android:
                        Utilities.Run("strip", "\"" + filename + "\"", null, dstBinaries, Utilities.RunOptions.ConsoleLogOutput);
                        break;
                    }
                    break;
                }
            }
            srcBinaries = Path.Combine(root, "physx", "compiler", preset, "sdk_source_bin", configuration);
            var additionalPhysXLibs = new[]
            {
                "FastXml",
                "LowLevel",
                "LowLevelAABB",
                "LowLevelDynamics",
                "PhysXTask",
                "SceneQuery",
                "SimulationController",
            };
            foreach (var additionalPhysXLib in additionalPhysXLibs)
            {
                var filenamePdb = suppressBitsPostfix ? string.Format("{0}{1}", binariesPrefix, additionalPhysXLib) : string.Format("{0}{1}_{2}", binariesPrefix, additionalPhysXLib, bits);
                filenamePdb += ".pdb";
                if (File.Exists(Path.Combine(srcBinaries, filenamePdb)))
                    Utilities.FileCopy(Path.Combine(srcBinaries, filenamePdb), Path.Combine(dstBinaries, filenamePdb));
            }
        }

        /// <inheritdoc />
        public override void Build(BuildOptions options)
        {
            root = options.IntermediateFolder;
            projectGenDir = Path.Combine(root, "physx");
            solutionFilesRoot = Path.Combine(root, "physx", "compiler");
            switch (BuildPlatform)
            {
            case TargetPlatform.Windows:
                projectGenPath = Path.Combine(projectGenDir, "generate_projects.bat");
                break;
            case TargetPlatform.Linux:
            case TargetPlatform.Mac:
                projectGenPath = Path.Combine(projectGenDir, "generate_projects.sh");
                break;
            default: throw new InvalidPlatformException(BuildPlatform);
            }

            // Get the source
            CloneGitRepoSingleBranch(root, "https://github.com/FlaxEngine/PhysX-5.git", "flax-master");

            foreach (var platform in options.Platforms)
            {
                foreach (var architecture in options.Architectures)
                {
                    BuildStarted(platform, architecture);
                    switch (platform)
                    {
                    case TargetPlatform.Windows:
                    {
                        if (architecture == TargetArchitecture.x64 || architecture == TargetArchitecture.ARM64)
                        {
                            if (WindowsPlatform.GetToolsets().Any(x => x.Key == WindowsPlatformToolset.v145))
                            {
                                try
                                {
                                    Build(options, architecture == TargetArchitecture.x64 ? "vc18win64" : "vc18win-arm64", platform, architecture);
                                }
                                catch (Exception e)
                                {
                                    Log.Warning($"Failed to generate VS2026 solution for PhysX, fallback to VS2022: {e.Message}");
                                    Build(options, architecture == TargetArchitecture.x64 ? "vc17win64" : "vc17win-arm64", platform, architecture);
                                }
                            }
                            else
                                Build(options, architecture == TargetArchitecture.x64 ? "vc17win64" : "vc17win-arm64", platform, architecture);
                        }
                        else
                            throw new InvalidArchitectureException(architecture);
                        break;
                    }
                    case TargetPlatform.Linux:
                    {
                        Build(options, "linux", platform, architecture);
                        break;
                    }
                    case TargetPlatform.PS4:
                    {
                        Utilities.DirectoryCopy(Path.Combine(GetBinariesFolder(options, platform), "Data", "PhysX"), root, true, true);
                        Build(options, "ps4", platform, TargetArchitecture.x64);
                        break;
                    }
                    case TargetPlatform.PS5:
                    {
                        Utilities.DirectoryCopy(Path.Combine(GetBinariesFolder(options, platform), "Data", "PhysX"), root, true, true);
                        Build(options, "ps5", platform, TargetArchitecture.x64);
                        break;
                    }
                    case TargetPlatform.XboxScarlett:
                    case TargetPlatform.XboxOne:
                    {
                        Build(options, "vc16win64", platform, TargetArchitecture.x64);
                        break;
                    }
                    case TargetPlatform.Android:
                    {
                        Build(options, "android", platform, TargetArchitecture.ARM64);
                        break;
                    }
                    case TargetPlatform.Switch:
                    {
                        Utilities.DirectoryCopy(Path.Combine(GetBinariesFolder(options, platform), "Data", "PhysX"), root, true, true);
                        Build(options, "switch64", platform, TargetArchitecture.ARM64);
                        break;
                    }
                    case TargetPlatform.Mac:
                    {
                        if (architecture == TargetArchitecture.x64)
                            Build(options, "mac64", platform, architecture);
                        else if (architecture == TargetArchitecture.ARM64)
                            Build(options, "mac-arm64", platform, architecture);
                        else
                            throw new InvalidArchitectureException(architecture);
                        break;
                    }
                    case TargetPlatform.iOS:
                    {
                        Build(options, "ios64", platform, TargetArchitecture.ARM64);
                        break;
                    }
                    case TargetPlatform.Web:
                    {
                        Build(options, "web32", platform, TargetArchitecture.x86);
                        break;
                    }
                    }
                }
            }

            // Copy header files
            var dstIncludePath = Path.Combine(options.ThirdPartyFolder, "PhysX");
            Directory.GetFiles(dstIncludePath, "*.h", SearchOption.AllDirectories).ToList().ForEach(File.Delete);
            Utilities.FileCopy(Path.Combine(root, "LICENSE.md"), Path.Combine(dstIncludePath, "License.txt"));
            Utilities.DirectoryCopy(Path.Combine(root, "physx", "include"), dstIncludePath, true, true);
        }
    }
}
