using System.CommandLine;
using System.Diagnostics;
using SimpleExec;
using Spectre.Console;
using BuildTool.Modules;

using Command = System.CommandLine.Command;

namespace BuildTool;

class Program
{
    /// <summary>
    /// Given a list of modules, returns a list with the order in which they need to be build.
    /// </summary>
    /// <param name="requestedModules">The list of modules we want to build.</param>
    /// <param name="outOrderedModules">The list will receive the modules in the order in which they need to be build.</param>
    /// <returns><see langword="true"/> if the modules have been ordered; <see langword="false"/>, if the graph could not be ordered.</returns>
    private static bool ComputeBuildOrder(List<Module> requestedModules, List<Module> outOrderedModules)
    {
        Dictionary<Module, int> indegrees = new(requestedModules.Count);
        Dictionary<Module, List<Module>> dependees = new(requestedModules.Count);

        Queue<Module> queue = new Queue<Module>(requestedModules.Count);

        // Get in-degrees and put 0-degree nodes into queue
        foreach (Module module in requestedModules)
        {
            int inDegree = module.Dependencies.Count;
            indegrees[module] = inDegree;

            if (inDegree == 0)
            {
                queue.Enqueue(module);
            }
            else
            {
                foreach (Module dependency in module.Dependencies)
                {
                    if (!dependees.TryGetValue(dependency, out List<Module>? dependeeList))
                    {
                        dependees[dependency] = dependeeList = new List<Module>();
                    }

                    dependeeList.Add(module);
                }
            }
        }

        while (queue.TryDequeue(out Module? currentModule))
        {
            outOrderedModules.Add(currentModule);

            if (dependees.TryGetValue(currentModule, out List<Module>? dependeeList))
            {
                foreach (var dependee in dependeeList)
                {
                    int dependeeIndegree = --indegrees[dependee];

                    Debug.Assert(dependeeIndegree >= 0);

                    if (dependeeIndegree == 0)
                    {
                        queue.Enqueue(dependee);
                    }
                }
            }
        }

        if (outOrderedModules.Count < requestedModules.Count)
        {
            // We have a cycle and can't order the dependencies.
            return false;
        }

        return true;
    }

    static async Task<int> Main(string[] args)
    {
        List<Module> modules = [new BeefWorkspaceModule(), new GlitchyEngineHelperModule(), new ScriptCoreModule(), new SetupNethostModule(), new Box2DModule(), new FreetypeStackModule(), new MSDFgenModule(), new AllModule()];

        // Find dependencies
        foreach (Module module in modules)
        {
            foreach (Type moduleDependencyType in module.DependencyTypes)
            {
                Module dependencyInstance = modules.Single(m => moduleDependencyType.IsInstanceOfType(m));
                module.Dependencies.Add(dependencyInstance);
            }
        }

        List<Module> orderedModules = new List<Module>(modules.Count);
        if (!ComputeBuildOrder(modules, orderedModules))
        {
            throw new Exception($"Could not compute build order. Are there any cylces? Unsorted modules: {string.Join(", ", modules.Except(orderedModules).Select(m => m.Name))}");
        }

        Option<bool> debugOption = new("--debug", "-d")
        {
            Description = "Defines that the specified modules should be compiled as debug builds."
        };
        Option<bool> releaseOption = new("--release", "-r")
        {
            Description = "Defines that the specified modules should be compiled as release builds."
        };
        Option<bool> rebuildOption = new("--force-rebuild", "-f")
        {
            Description = "Defines that the specified modules should be rebuild."
        };
        Option<bool> noDependenciesOption = new("--no-dependencies", "-n")
        {
            Description = "Defines that only the specified modules are build, but not their dependencies (unless they are also specified)."
        };

        Option<DirectoryInfo> workspaceRootOption = new("--workspace", "-w")
        {
            Description = "The relative or absolute path to the root directory of the workspace. If unset, the current working directory will be used.",
            DefaultValueFactory = _ => new DirectoryInfo(Environment.CurrentDirectory)
        };
        workspaceRootOption.Validators.Add(result =>
        {
            if (result.GetValueOrDefault<DirectoryInfo>() is not DirectoryInfo workspaceRoot)
            {
                result.AddError("The workspace directory doesn't exist.");
                return;
            }

            if (!workspaceRoot.Exists)
            {
                result.AddError($"The workspace directory '{workspaceRoot.FullName}' doesn't exist.");
            }

            // Minimal sanity check: Is it actually a beef workspace? (Does BeefSpace.toml exist)
            string beefSpaceFile = Path.Combine(workspaceRoot.FullName, "BeefSpace.toml");
            if (!File.Exists(beefSpaceFile))
            {
                result.AddError($"The directory '{workspaceRoot.FullName}' doesn't seem to be a workspace. Either invoke it from the root of the workspace, or use the '--workspace' option.");
            }
        });

        Argument<string[]> modulesArgument = new("modules")
        {
            Description = "The modules to build.",
            // DefaultValueFactory = _ => [AllModule.ModuleName]
        };

        modulesArgument.Validators.Add(result =>
        {
            if (result.GetValue(modulesArgument) is string[] modulesToBuild)
            {
                foreach (string moduleToBuild in modulesToBuild)
                {
                    if (!modules.Any(m => m.Name.Equals(moduleToBuild, StringComparison.OrdinalIgnoreCase)))
                    {
                        result.AddError($"The module \"{moduleToBuild}\" doesn't exists.");
                    }
                }
            }
        });
        modulesArgument.CompletionSources.Add(completionContext =>
        {
            string[] alreadySpecifiedModules = completionContext.ParseResult.GetValue(modulesArgument) ?? [];

            return modules.Select(m => m.Name) // Get available Module names.
                .Where(moduleName => moduleName.StartsWith(completionContext.WordToComplete)) // Only take the modules that start with the word to complete.
                .Where(moduleName => !alreadySpecifiedModules.Contains(moduleName)); // Remove the modules that already are specified.
        });

        RootCommand rootCommand = new("Builds the engine and it's modules.");

        Command buildCommand = new("build",  "Builds the specified modules. At least either '--debug' or '--release' or both must be specified.");
        rootCommand.Add(buildCommand);

        buildCommand.Options.Add(debugOption);
        buildCommand.Options.Add(releaseOption);
        buildCommand.Options.Add(rebuildOption);
        buildCommand.Options.Add(noDependenciesOption);
        buildCommand.Options.Add(workspaceRootOption);
        buildCommand.Arguments.Add(modulesArgument);

        buildCommand.Validators.Add(result =>
        {
            bool buildDebug = result.GetValue(debugOption);
            bool buildRelease = result.GetValue(releaseOption);

            if (!buildDebug && !buildRelease)
            {
                result.AddError("Either \"--debug\" or \"--release\" option, or both, must be specified.");
            }
        });

        buildCommand.SetAction(parseResult =>
            BuildModules(
                orderedModules,
            parseResult.GetValue(modulesArgument)!,
            parseResult.GetValue(workspaceRootOption)!,
            parseResult.GetValue(rebuildOption),
            parseResult.GetValue(noDependenciesOption),
            parseResult.GetValue(debugOption),
            parseResult.GetValue(releaseOption)));

        Command listModulesCommand = new("list");
        rootCommand.Add(listModulesCommand);
        listModulesCommand.SetAction(result =>
        {
            Console.WriteLine("Following modules can be build:");

            foreach (Module module in modules)
            {
                Console.WriteLine(module.Name);
            }

            Console.WriteLine("Build order:");
            Console.WriteLine(string.Join(" -> ", orderedModules.Select(m => m.Name)));
        });

        ParseResult parseResult = rootCommand.Parse(args);
        return await parseResult.InvokeAsync();
    }

    private static async Task BuildModules(List<Module> allModulesOrdered, string[] moduleNamesToBuild, DirectoryInfo workspaceRoot, bool forceRebuild, bool noDependencies, bool debug, bool release)
    {
        HashSet<Module> modulesToBuild = new HashSet<Module>(allModulesOrdered.Count);

        if (moduleNamesToBuild.Length == 0)
        {
            Console.WriteLine("Building all modules.");

            modulesToBuild.UnionWith(allModulesOrdered);
        }
        else
        {
            Queue<Module> modulesToAdd = new Queue<Module>(allModulesOrdered.Where(m =>
                moduleNamesToBuild.Contains(m.Name, StringComparer.OrdinalIgnoreCase)));

            while (modulesToAdd.TryDequeue(out Module? moduleToAdd))
            {
                if (modulesToBuild.Add(moduleToAdd) && !noDependencies)
                {
                    foreach (Module dependency in moduleToAdd.Dependencies)
                    {
                        modulesToAdd.Enqueue(dependency);
                    }
                }
            }
        }

        List<Module> acutalModuleOrder = allModulesOrdered.Intersect(modulesToBuild).ToList();

        string cacheFilePath = Path.Combine(workspaceRoot.FullName, "build", ".build_tool.json");

        ModuleCache moduleCache = ModuleCache.Load(cacheFilePath);

        Console.WriteLine($"Building modules: {string.Join(", ", acutalModuleOrder.Select(m => m.Name))}");
        await Task.Delay(1000);

        Console.WriteLine($"Workspace:  {workspaceRoot.FullName}");
        BuildInfo buildInfo = new(new WorkingDirectoryHistory(workspaceRoot.FullName), forceRebuild, debug, release);

        foreach (Module module in acutalModuleOrder)
        {
            string message = $"Building module: [bold italic blue]{module.Name}[/]";
            
            Console.WriteLine();
            Console.WriteLine(new string('-', message.Length));
            AnsiConsole.MarkupLine(message);
            Console.WriteLine(new string('-', message.Length));
            Console.WriteLine();
            
            buildInfo.WorkingDirectory.NavigateToIndex(0);

            // create a copy of the build info updated with the module's watched sources and cache info (last build time etc.)
            BuildInfo moduleBuildInfo = module.GetBuildInfo(buildInfo, moduleCache);
            
            if (!moduleBuildInfo.BuildDebug && !moduleBuildInfo.BuildRelease)
            {
                AnsiConsole.MarkupLine($"\n[green]Module [bold italic]{module.Name}[/] is up to date, skipping.[/]\n");
                continue;
            }

            try
            {
                await module.Run(moduleBuildInfo);

                AnsiConsole.MarkupLine($"\n[green]Module [bold italic]{module.Name}[/] completed successfully.[/]\n");

                module.UpdateCacheInfo(moduleBuildInfo, moduleCache, true);
            }
            catch (ExitCodeException e)
            {
                AnsiConsole.MarkupLine(
                    $"\n[bold red]Error:[/] Module [bold italic]{module.Name}[/] failed with exit code: [red]{e.ExitCode}[/]\n");

                module.UpdateCacheInfo(moduleBuildInfo, moduleCache, false);

                break;
            }
            finally
            {
                moduleCache.Save(cacheFilePath);
            }
        }
    }
}
