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

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Flax.Build.Graph;

namespace Flax.Build
{
    static partial class Builder
    {
        private static void BuildTargetDotNet(RulesAssembly rules, TaskGraph graph, Target target, Platform platform, TargetConfiguration configuration)
        {
            throw new NotImplementedException("TODO: building C# targets");
        }

        private static void BuildTargetBindings(RulesAssembly rules, TaskGraph graph, BuildData buildData)
        {
            var workspaceRoot = buildData.TargetOptions.WorkingDirectory;
            var args = new List<string>();
            var referencesToCopy = new HashSet<KeyValuePair<string, string>>();
            var buildOptions = buildData.TargetOptions;
            foreach (var binaryModule in buildData.BinaryModules)
            {
                var binaryModuleName = binaryModule.Key;
                using (new ProfileEventScope(binaryModuleName))
                {
                    // TODO: add support for extending this code and support generating bindings projects for other scripting languages
                    var project = GetModuleProject(binaryModule.First(), buildData);

                    // Get source files
                    var sourceFiles = new List<string>();
                    foreach (var module in binaryModule)
                        sourceFiles.AddRange(buildData.Modules[module].SourceFiles.Where(x => x.EndsWith(".cs")));
                    sourceFiles.RemoveAll(x => x.EndsWith(BuildFilesPostfix));
                    var moduleGen = Path.Combine(project.ProjectFolderPath, "Source", binaryModuleName + ".Gen.cs");
                    if (!sourceFiles.Contains(moduleGen))
                        sourceFiles.Add(moduleGen);
                    sourceFiles.Sort();

                    // Setup build options
                    var buildPlatform = Platform.BuildPlatform.Target;
                    var outputPath = Path.GetDirectoryName(buildData.Target.GetOutputFilePath(buildOptions));
                    var outputFile = Path.Combine(outputPath, binaryModuleName + ".CSharp.dll");
                    var outputDocFile = Path.Combine(outputPath, binaryModuleName + ".CSharp.xml");
                    var monoRoot = Path.Combine(Globals.EngineRoot, "Source", "Platforms", "Editor", "Windows", "Mono");
                    var cscPath = Path.Combine(monoRoot, "lib", "mono", "4.5", "csc.exe");
                    var exePath = buildPlatform == TargetPlatform.Windows ? Path.Combine(monoRoot, "bin", "mono.exe") : "mono";
                    var referenceAssemblies = Path.Combine(monoRoot, "lib", "mono", "4.5-api");
                    var references = new HashSet<string>(buildOptions.ScriptingAPI.FileReferences);
                    foreach (var module in binaryModule)
                    {
                        if (!buildData.Modules.TryGetValue(module, out var moduleBuildOptions))
                            continue;

                        // Find references based on the modules dependencies
                        foreach (var dependencyName in moduleBuildOptions.PublicDependencies.Concat(moduleBuildOptions.PrivateDependencies))
                        {
                            var dependencyModule = buildData.Rules.GetModule(dependencyName);
                            if (dependencyModule != null &&
                                !string.IsNullOrEmpty(dependencyModule.BinaryModuleName) &&
                                dependencyModule.BinaryModuleName != binaryModuleName &&
                                buildData.Modules.TryGetValue(dependencyModule, out var dependencyModuleOptions))
                            {
                                foreach (var x in buildData.BinaryModules)
                                {
                                    if (x.Key == null || x.Key != dependencyModule.BinaryModuleName)
                                        continue;

                                    // Reference module output binary
                                    references.Add(Path.Combine(outputPath, dependencyModule.BinaryModuleName + ".CSharp.dll"));
                                }
                                foreach (var e in buildData.ReferenceBuilds)
                                {
                                    foreach (var q in e.Value.BuildInfo.BinaryModules)
                                    {
                                        if (q.Name == dependencyModule.BinaryModuleName && !string.IsNullOrEmpty(q.ManagedPath))
                                        {
                                            // Reference binary module build build for referenced target
                                            references.Add(q.ManagedPath);
                                            break;
                                        }
                                    }
                                }
                            }
                        }
                    }

                    // Setup C# compiler arguments
                    args.Clear();
                    args.Add("/nologo");
                    args.Add("/target:library");
                    args.Add("/platform:AnyCPU");
                    args.Add("/debug+");
                    args.Add("/debug:portable");
                    args.Add("/errorreport:prompt");
                    args.Add("/preferreduilang:en-US");
                    args.Add("/highentropyva+");
                    args.Add("/deterministic");
                    args.Add("/nostdlib+");
                    args.Add("/errorendlocation");
                    args.Add("/utf8output");
                    args.Add("/warn:4");
                    args.Add("/unsafe");
                    args.Add("/fullpaths");
                    args.Add("/langversion:7.3");
                    if (buildOptions.ScriptingAPI.IgnoreMissingDocumentationWarnings)
                        args.Add("-nowarn:1591");
                    args.Add(buildData.Configuration == TargetConfiguration.Debug ? "/optimize-" : "/optimize+");
                    args.Add(string.Format("/out:\"{0}\"", outputFile));
                    args.Add(string.Format("/doc:\"{0}\"", outputDocFile));
                    if (buildOptions.ScriptingAPI.Defines.Count != 0)
                        args.Add("/define:" + string.Join(";", buildOptions.ScriptingAPI.Defines));
                    if (buildData.Configuration == TargetConfiguration.Debug)
                        args.Add("/define:DEBUG");
                    args.Add(string.Format("/reference:\"{0}{1}mscorlib.dll\"", referenceAssemblies, Path.DirectorySeparatorChar));
                    foreach (var reference in buildOptions.ScriptingAPI.SystemReferences)
                        args.Add(string.Format("/reference:\"{0}{2}{1}.dll\"", referenceAssemblies, reference, Path.DirectorySeparatorChar));
                    foreach (var reference in references)
                        args.Add(string.Format("/reference:\"{0}\"", reference));
                    foreach (var sourceFile in sourceFiles)
                        args.Add("\"" + sourceFile + "\"");

                    // Generate response file with source files paths and compilation arguments
                    string responseFile = Path.Combine(buildOptions.IntermediateFolder, binaryModuleName + ".CSharp.response");
                    Utilities.WriteFileIfChanged(responseFile, string.Join(Environment.NewLine, args));

                    // Create C# compilation task
                    var task = graph.Add<Task>();
                    task.PrerequisiteFiles.Add(responseFile);
                    task.PrerequisiteFiles.AddRange(sourceFiles);
                    task.PrerequisiteFiles.AddRange(references);
                    task.ProducedFiles.Add(outputFile);
                    task.WorkingDirectory = workspaceRoot;
                    task.CommandPath = exePath;
                    task.CommandArguments = $"\"{cscPath}\" /noconfig @\"{responseFile}\"";
                    task.InfoMessage = "Compiling C# API for " + binaryModuleName;
                    task.Cost = task.PrerequisiteFiles.Count;

                    // Copy referenced assemblies
                    foreach (var reference in buildOptions.ScriptingAPI.FileReferences)
                    {
                        var dstFile = Path.Combine(outputPath, Path.GetFileName(reference));
                        if (dstFile == reference)
                            continue;

                        referencesToCopy.Add(new KeyValuePair<string, string>(dstFile, reference));
                    }
                }
            }

            // Copy files (using hash set to prevent copying the same file twice when building multiple scripting modules using the same files)
            foreach (var e in referencesToCopy)
            {
                var dst = e.Key;
                var src = e.Value;
                graph.AddCopyFile(dst, src);

                var srcPdb = Path.ChangeExtension(src, "pdb");
                if (File.Exists(srcPdb))
                    graph.AddCopyFile(Path.ChangeExtension(dst, "pdb"), srcPdb);

                var srcXml = Path.ChangeExtension(src, "xml");
                if (File.Exists(srcXml))
                    graph.AddCopyFile(Path.ChangeExtension(dst, "xml"), srcXml);
            }
        }
    }
}
