// 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.Graph;
using Flax.Build.NativeCpp;

namespace Flax.Build
{
    /// <summary>
    /// The modules and targets building utility.
    /// </summary>
    public static partial class Builder
    {
        private static void CleanDirectory(DirectoryInfo dir)
        {
            var subdirs = dir.GetDirectories();
            foreach (var subdir in subdirs)
            {
                CleanDirectory(subdir);
            }

            var files = dir.GetFiles();
            foreach (var file in files)
            {
                try
                {
                    file.Delete();
                }
                catch (UnauthorizedAccessException)
                {
                    File.SetAttributes(file.FullName, FileAttributes.Normal);
                    file.Delete();
                }
                catch
                {
                    // Skip errors
                }
            }

            try
            {
                dir.Delete();
            }
            catch
            {
                // Skip errors
            }
        }

        /// <summary>
        /// Cleans the build system cache and intermediate results.
        /// </summary>
        public static void Clean()
        {
            using (new ProfileEventScope("Clean"))
            {
                // Clear task graph cache
                var project = Globals.Project;
                TaskGraph graph;
                if (project == null)
                {
                    graph = new TaskGraph(Globals.Root);
                    graph.CleanCache();
                    return;
                }
                graph = new TaskGraph(project.ProjectFolderPath);
                graph.CleanCache();

                // Pick targets to clean
                var customBuildTargets = Configuration.BuildTargets;
                var projectTargets = GetProjectTargets(project);
                var targets = customBuildTargets == null ? projectTargets : projectTargets.Where(target => customBuildTargets.Contains(target.Name)).ToArray();
                foreach (var target in targets)
                {
                    // Pick configurations to clean
                    TargetConfiguration[] configurations = Configuration.BuildConfigurations;
                    if (configurations != null)
                    {
                        foreach (var configuration in configurations)
                        {
                            if (!target.Configurations.Contains(configuration))
                                throw new Exception(string.Format("Target {0} does not support {1} configuration.", target.Name, configuration));
                        }
                    }
                    else
                    {
                        configurations = target.Configurations;
                    }

                    foreach (var configuration in configurations)
                    {
                        // Pick platforms to clean
                        TargetPlatform[] platforms = Configuration.BuildPlatforms;
                        if (platforms != null)
                        {
                            foreach (var platform in platforms)
                            {
                                if (!target.Platforms.Contains(platform))
                                    throw new Exception(string.Format("Target {0} does not support {1} platform.", target.Name, platform));
                            }
                        }
                        else
                        {
                            platforms = target.Platforms;
                        }

                        foreach (var targetPlatform in platforms)
                        {
                            if (!Platform.BuildPlatform.CanBuildPlatform(targetPlatform))
                                continue;

                            // Pick architectures to build
                            TargetArchitecture[] architectures = Configuration.BuildArchitectures;
                            if (architectures != null)
                            {
                                foreach (var e in architectures)
                                {
                                    if (!target.Architectures.Contains(e))
                                        throw new Exception(string.Format("Target {0} does not support {1} architecture.", target.Name, e));
                                }
                            }
                            else
                            {
                                architectures = target.GetArchitectures(targetPlatform);
                            }

                            foreach (var architecture in architectures)
                            {
                                if (!Platform.IsPlatformSupported(targetPlatform, architecture))
                                    continue;
                                //throw new Exception(string.Format("Platform {0} {1} is not supported.", targetPlatform, architecture));

                                var platform = Platform.GetPlatform(targetPlatform);
                                var toolchain = platform.GetToolchain(architecture);
                                var targetBuildOptions = GetBuildOptions(target, toolchain.Platform, toolchain, toolchain.Architecture, configuration, project.ProjectFolderPath, string.Empty);

                                // Delete all intermediate files
                                var intermediateFolder = new DirectoryInfo(targetBuildOptions.IntermediateFolder);
                                if (intermediateFolder.Exists)
                                {
                                    Log.Info("Removing: " + targetBuildOptions.IntermediateFolder);
                                    CleanDirectory(intermediateFolder);
                                    intermediateFolder.Create();
                                }

                                // Delete all output files
                                var outputFolder = new DirectoryInfo(targetBuildOptions.OutputFolder);
                                if (outputFolder.Exists)
                                {
                                    Log.Info("Removing: " + targetBuildOptions.OutputFolder);
                                    CleanDirectory(outputFolder);
                                    outputFolder.Create();
                                }
                            }
                        }
                    }
                }
            }
        }

        /// <summary>
        /// Gets the list of targets that can be used when building a given project.
        /// </summary>
        /// <param name="project">The project.</param>
        /// <returns>The array of targets that can be build.</returns>
        public static Target[] GetProjectTargets(ProjectInfo project)
        {
            var rules = GenerateRulesAssembly();
            var sourcePath = Path.Combine(project.ProjectFolderPath, "Source");
            return rules.Targets.Where(target => target.FolderPath.StartsWith(sourcePath)).ToArray();
        }

        /// <summary>
        /// Gets the list of modules that are located under a given project (checks for modules located in the given project Source folder).
        /// </summary>
        /// <param name="project">The project.</param>
        /// <returns>The array of modules that are contained in this project.</returns>
        public static Module[] GetProjectModules(ProjectInfo project)
        {
            var rules = GenerateRulesAssembly();
            var sourcePath = Path.Combine(project.ProjectFolderPath, "Source");
            return rules.Modules.Where(module => module.FolderPath.StartsWith(sourcePath)).ToArray();
        }

        /// <summary>
        /// Gets the project that contains a given module (checks for modules located in the given project Source folder).
        /// </summary>
        /// <param name="module">The module.</param>
        /// <param name="project">The project.</param>
        /// <returns>The found project.</returns>
        public static ProjectInfo GetModuleProject(Module module, ProjectInfo project)
        {
            if (project == null)
                return null;
            var sourcePath = Path.Combine(project.ProjectFolderPath, "Source");
            if (module.FolderPath.StartsWith(sourcePath))
                return project;
            foreach (var reference in project.References)
            {
                var result = GetModuleProject(module, reference.Project);
                if (result != null)
                    return result;
            }
            return null;
        }

        /// <summary>
        /// Gets the project that contains a given module (checks for modules located in the given project Source folder).
        /// </summary>
        /// <param name="module">The module.</param>
        /// <param name="buildData">The build data.</param>
        /// <returns>The found project.</returns>
        public static ProjectInfo GetModuleProject(Module module, BuildData buildData)
        {
            return GetModuleProject(module, buildData.Project);
        }

        /// <summary>
        /// Checks if the project that contains a given module (checks for modules located in the given project Source folder).
        /// </summary>
        /// <param name="module">The module.</param>
        /// <param name="project">The project to check.</param>
        /// <returns>True if project contains that module inside, otherwise it's external or referenced.</returns>
        public static bool IsModuleFromProject(Module module, ProjectInfo project)
        {
            return GetModuleProject(module, project) == project;
        }

        /// <summary>
        /// Builds the targets.
        /// </summary>
        /// <returns>True if failed, otherwise false.</returns>
        public static bool BuildTargets()
        {
            bool failed = false;

            var rules = GenerateRulesAssembly();
            var project = Globals.Project;
            if (project == null)
            {
                Log.Warning("Missing project");
                return true;
            }

            using (new ProfileEventScope("BuildTargets"))
            {
                // Pick targets to build
                var customBuildTargets = Configuration.BuildTargets;
                var projectTargets = GetProjectTargets(project);
                var targets = customBuildTargets == null ? projectTargets : projectTargets.Where(target => customBuildTargets.Contains(target.Name)).ToArray();
                if (customBuildTargets != null)
                {
                    var targetsList = new List<Target>(customBuildTargets.Length);
                    foreach (var customBuildTarget in customBuildTargets)
                    {
                        var target = projectTargets.FirstOrDefault(x => string.Equals(x.Name, customBuildTarget, StringComparison.InvariantCultureIgnoreCase));
                        if (target != null)
                            targetsList.Add(target);
                        else
                            Log.Error("Missing target " + customBuildTarget);
                    }
                    targets = targetsList.ToArray();
                }
                if (targets.Length == 0)
                    Log.Warning("No targets to build");

                using (new ProfileEventScope("LoadIncludesCache"))
                {
                    IncludesCache.LoadCache();
                }

                // Create task graph for building all targets
                var graph = new TaskGraph(project.ProjectFolderPath);
                foreach (var target in targets)
                {
                    target.PreBuild();

                    // Pick configurations to build
                    TargetConfiguration[] configurations = Configuration.BuildConfigurations;
                    if (configurations != null)
                    {
                        foreach (var configuration in configurations)
                        {
                            if (!target.Configurations.Contains(configuration))
                                throw new Exception(string.Format("Target {0} does not support {1} configuration.", target.Name, configuration));
                        }
                    }
                    else
                    {
                        configurations = target.Configurations;
                    }

                    foreach (var configuration in configurations)
                    {
                        // Pick platforms to build
                        TargetPlatform[] platforms = Configuration.BuildPlatforms;
                        if (platforms != null)
                        {
                            foreach (var platform in platforms)
                            {
                                if (!target.Platforms.Contains(platform))
                                    throw new Exception(string.Format("Target {0} does not support {1} platform.", target.Name, platform));
                            }
                        }
                        else
                        {
                            platforms = target.Platforms;
                        }

                        foreach (var targetPlatform in platforms)
                        {
                            // Pick architectures to build
                            TargetArchitecture[] architectures = Configuration.BuildArchitectures;
                            if (architectures != null)
                            {
                                foreach (var e in architectures)
                                {
                                    if (!target.Architectures.Contains(e))
                                        throw new Exception(string.Format("Target {0} does not support {1} architecture.", target.Name, e));
                                }
                            }
                            else
                            {
                                architectures = target.GetArchitectures(targetPlatform);
                            }

                            foreach (var architecture in architectures)
                            {
                                if (!Platform.IsPlatformSupported(targetPlatform, architecture))
                                    continue;
                                //throw new Exception(string.Format("Platform {0} {1} is not supported.", targetPlatform, architecture));

                                var platform = Platform.GetPlatform(targetPlatform);

                                // Special case: building C# bindings only (eg. when building Linux game on Windows without C++ scripting or for C#-only projects)
                                if (Configuration.BuildBindingsOnly || (project.IsCSharpOnlyProject && platform.HasModularBuildSupport))
                                {
                                    Log.Info("Building C# only");
                                    using (new ProfileEventScope(target.Name))
                                    {
                                        Log.Info(string.Format("Building target {0} in {1} for {2} {3}", target.Name, configuration, targetPlatform, architecture));

                                        var buildContext = new Dictionary<Target, BuildData>();
                                        switch (target.Type)
                                        {
                                        case TargetType.NativeCpp:
                                            BuildTargetNativeCppBindingsOnly(rules, graph, target, buildContext, platform, architecture, configuration);
                                            break;
                                        case TargetType.DotNet:
                                        case TargetType.DotNetCore:
                                            BuildTargetDotNet(rules, graph, target, platform, configuration);
                                            break;
                                        default: throw new ArgumentOutOfRangeException();
                                        }
                                    }
                                    continue;
                                }

                                var toolchain = platform.GetToolchain(architecture);
                                using (new ProfileEventScope(target.Name))
                                {
                                    Log.Info(string.Format("Building target {0} in {1} for {2} {3}", target.Name, configuration, targetPlatform, architecture));

                                    var buildContext = new Dictionary<Target, BuildData>();
                                    switch (target.Type)
                                    {
                                    case TargetType.NativeCpp:
                                        BuildTargetNativeCpp(rules, graph, target, buildContext, toolchain, configuration);
                                        break;
                                    case TargetType.DotNet:
                                    case TargetType.DotNetCore:
                                        BuildTargetDotNet(rules, graph, target, toolchain.Platform, configuration);
                                        break;
                                    default: throw new ArgumentOutOfRangeException();
                                    }
                                }
                            }
                        }
                    }
                }

                // Prepare tasks for the execution
                using (new ProfileEventScope("PrepareTasks"))
                {
                    using (new ProfileEventScope("Setup"))
                        graph.Setup();

                    using (new ProfileEventScope("SortTasks"))
                        graph.SortTasks();

                    using (new ProfileEventScope("LoadCache"))
                        graph.LoadCache();
                }

                // Execute tasks
                int executedTasksCount;
                using (new ProfileEventScope("ExecuteTasks"))
                {
                    failed |= graph.Execute(out executedTasksCount);
                }

                if (executedTasksCount != 0)
                {
                    // Save graph execution result cache
                    using (new ProfileEventScope("SaveCache"))
                    {
                        graph.SaveCache();
                    }
                }

                using (new ProfileEventScope("SaveIncludesCache"))
                {
                    IncludesCache.SaveCache();
                }

                foreach (var target in targets)
                {
                    target.PostBuild();
                }
            }

            return failed;
        }

        private static void DeployFiles(TaskGraph graph, Target target, BuildOptions targetBuildOptions, string outputPath)
        {
            using (new ProfileEventScope("DeployFiles"))
            {
                foreach (var srcFile in targetBuildOptions.OptionalDependencyFiles.Where(File.Exists).Union(targetBuildOptions.DependencyFiles))
                {
                    var dstFile = Path.Combine(outputPath, Path.GetFileName(srcFile));
                    graph.AddCopyFile(dstFile, srcFile);
                }

                if (targetBuildOptions.NugetPackageReferences.Any())
                {
                    // Find all packages to deploy (incl. dependencies) and restore if needed
                    var nugetPath = Utilities.GetNugetPackagesPath();
                    Log.Verbose($"Deploying NuGet packages from {nugetPath}");
                    var restoreOnce = true;
                    var nugetFiles = new HashSet<string>();
                    foreach (var reference in targetBuildOptions.NugetPackageReferences)
                    {
                        var folder = reference.GetLibFolder(nugetPath);
                        if (!Directory.Exists(folder) && restoreOnce)
                        {
                            // Package binaries folder is missing so restore packages (incl. dependency packages)
                            RestoreNugetPackages(graph, target, targetBuildOptions);
                            restoreOnce = false;
                        }

                        DeployNuGetPackage(nugetPath, targetBuildOptions, nugetFiles, reference, folder);
                    }

                    // Copy libraries from all referenced packages to the output folder
                    foreach (var file in nugetFiles)
                    {
                        var dstFile = Path.Combine(outputPath, Path.GetFileName(file));
                        graph.AddCopyFile(dstFile, file);
                    }
                }
            }
        }

        private static void RestoreNugetPackages(TaskGraph graph, Target target, BuildOptions targetBuildOptions)
        {
            // Generate a dummy csproj file to restore package from it
            var csprojPath = Path.Combine(targetBuildOptions.IntermediateFolder, "nuget.restore.csproj");
            var dotnetSdk = DotNetSdk.Instance;
            var csProjectFileContent = new StringBuilder();
            csProjectFileContent.AppendLine("<Project Sdk=\"Microsoft.NET.Sdk\">");
            csProjectFileContent.AppendLine("  <PropertyGroup>");
            csProjectFileContent.AppendLine($"    <TargetFramework>net{dotnetSdk.Version.Major}.{dotnetSdk.Version.Minor}</TargetFramework>");
            csProjectFileContent.AppendLine("    <IsPackable>false</IsPackable>");
            csProjectFileContent.AppendLine("    <EnableDefaultItems>false</EnableDefaultItems>");
            csProjectFileContent.AppendLine("    <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>");
            csProjectFileContent.AppendLine("    <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>");
            csProjectFileContent.AppendLine("    <EnableBaseIntermediateOutputPathMismatchWarning>false</EnableBaseIntermediateOutputPathMismatchWarning>");
            csProjectFileContent.AppendLine("    <GenerateAssemblyInfo>false</GenerateAssemblyInfo>");
            csProjectFileContent.AppendLine($"    <LangVersion>{dotnetSdk.CSharpLanguageVersion}</LangVersion>");
            csProjectFileContent.AppendLine("    <FileAlignment>512</FileAlignment>");
            csProjectFileContent.AppendLine("    <RestorePackages>true</RestorePackages>");
            csProjectFileContent.AppendLine("  </PropertyGroup>");
            csProjectFileContent.AppendLine("  <ItemGroup>");
            foreach (var reference in targetBuildOptions.NugetPackageReferences)
                csProjectFileContent.AppendLine($"    <PackageReference Include=\"{reference.Name}\" Version=\"{reference.Version}\" />");
            csProjectFileContent.AppendLine("  </ItemGroup>");
            csProjectFileContent.AppendLine("</Project>");
            Utilities.WriteFileIfChanged(csprojPath, csProjectFileContent.ToString());

            // Restore packages using dotnet CLI (synchronous to prevent task ordering issues on C# library building)
            Log.Info($"Restoring NuGet packages for target {target.Name}");
            Utilities.Run(Utilities.GetDotNetPath(), $"restore \"{csprojPath}\"", null, null, Utilities.RunOptions.DefaultTool);
        }

        private static void DeployNuGetPackage(string nugetPath, BuildOptions targetBuildOptions, HashSet<string> nugetFiles, NugetPackage package, string folder = null)
        {
            // Deploy library
            var path = package.GetLibPath(nugetPath, folder);
            if (!File.Exists(path))
                return;
            Log.Verbose($"Deploying NuGet package {package.Name}, {package.Version}, {package.Framework}");
            nugetFiles.Add(path);

            // Copy additional files (if included)
            path = Path.ChangeExtension(path, "xml");
            if (File.Exists(path))
                nugetFiles.Add(path);
            path = Path.ChangeExtension(path, "pdb");
            if (targetBuildOptions.Configuration != TargetConfiguration.Release && File.Exists(path))
                nugetFiles.Add(path);

            // Read package dependencies
            var nuspecFile = package.GetNuspecPath(nugetPath);
            if (File.Exists(nuspecFile))
            {
                var doc = System.Xml.Linq.XDocument.Load(nuspecFile);
                var root = (System.Xml.Linq.XElement)doc.FirstNode;
                var metadataNode = root.Descendants().First(x => x.Name.LocalName== "metadata");
                var dependenciesNode = metadataNode.Descendants().First(x => x.Name.LocalName == "dependencies");
                var groupNode = dependenciesNode.Descendants().FirstOrDefault(x => x.Attribute("targetFramework")?.Value == package.Framework);
                if (groupNode == null)
                {
                    Log.Warning($"Cannot find framework {package.Framework} inside NuGet package {package.Name}, {package.Version}");
                    return;
                }
                foreach (var dependency in groupNode.Descendants())
                {
                    if (dependency.Name.LocalName != "dependency")
                        continue;

                    // Deploy dependency package
                    var dependencyId = dependency.Attribute("id").Value;
                    var dependencyVersion = dependency.Attribute("version").Value;
                    DeployNuGetPackage(nugetPath, targetBuildOptions, nugetFiles, new NugetPackage { Name = dependencyId, Version = dependencyVersion, Framework = package.Framework } );
                }
            }
        }
    }
}
