using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Text.RegularExpressions;
using CommandLine;
using LibGit2Sharp;
using Microsoft.Build.BuildEngine;
using Microsoft.Build.Evaluation;
using Microsoft.Build.Execution;
using Microsoft.Build.Framework;
using Mono.Unix.Native;
using Project = Microsoft.Build.BuildEngine.Project;
using UnixSyscall = Mono.Unix.Native.Syscall;

namespace CitizenMP.Server.Installer
{
    internal static class Program
    {
        // TODO: Get rid of this if possible
        internal static readonly Signature GitSignature = new Signature("Downloader", "downloader@localhost",
            new DateTimeOffset());

        private static void PrepareDirectory(DirectoryInfo sourcePath, DirectoryInfo targetPath)
        {
            if (!sourcePath.Exists)
            {
                throw new DirectoryNotFoundException("Source directory " + sourcePath.FullName + " does not exist.");
            }

            targetPath.Create();

            foreach (var file in sourcePath.EnumerateFiles())
            {
                var overwrite = !file.Extension.Equals("yml", StringComparison.OrdinalIgnoreCase);
                var target = Path.Combine(targetPath.FullName, file.Name);

                // If file exists and should not be directly overwritten, just remember the user to manually update the file as needed.
                if (File.Exists(target) && !overwrite)
                {
                    var oldTarget = target;
                    target += ".dist";
                    Console.Error.WriteLine(
                        "WARNING: File {0} needs a manual update! Compare with {1} and rewrite your file.", oldTarget,
                        target);
                }

                file.CopyTo(target, overwrite);
            }

            foreach (var subdirectory in sourcePath.EnumerateDirectories())
            {
                PrepareDirectory(subdirectory, targetPath.CreateSubdirectory(subdirectory.Name));
            }
        }

        private static int Main(string[] args)
        {
            // Parse cmdline arguments
            var options = new CommandLineOptions();
            //args = args.DefaultIfEmpty("--help").ToArray();
            if (!Parser.Default.ParseArgumentsStrict(args, options, () => { Environment.Exit(-2); }))
            {
                return -2;
            }

            if (options.ShowVersion)
            {
                var version =
                    Regex.Match(
                        Assembly.GetExecutingAssembly()
                            .GetCustomAttributes(typeof (AssemblyInformationalVersionAttribute), false)
                            .OfType<AssemblyInformationalVersionAttribute>().First().InformationalVersion,
                            @"^(?<major>[0-9]+)\.(?<minor>[0-9]+)\.(?<revision>[0-9]+)(\-(?<prerelease>[A-z0-9\.]+))?(\+(?<meta>.+?))?$");

                var meta = new Queue<string>(version.Groups["meta"].Value.Split('.'));
                while (meta.Any() && meta.First() != "Branch")
                {
                    meta.Dequeue();
                }

                var metaDict = new Dictionary<string, string>();
                while (meta.Any())
                {
                    var name = meta.Dequeue();
                    var value = meta.Dequeue();
                    if (meta.Any() && char.IsDigit(meta.First().First()))
                    {
                        value += "." + meta.Dequeue();
                    }
                    metaDict.Add(name, value);
                }

                Console.WriteLine("{0}.{1}.{2}{3}{4}",
                    version.Groups["major"].Value,
                    version.Groups["minor"].Value,
                    version.Groups["revision"].Value,
                    version.Groups["prerelease"].Success ? "-" + version.Groups["prerelease"].Value : "",
                    metaDict.Any() ? " (" + metaDict["Sha"].Substring(0, 7) + ")" : "");
                return 0;
            }

            if (string.IsNullOrEmpty(options.OutputPath))
            {
                Console.Error.WriteLine("ERROR: No output directory given.");
                Console.Write(options.GetUsage());
                return -2;
            }

            var sourceDirectory = new DirectoryInfo(options.SourceDir);
            var dataSourceDirectory = sourceDirectory
                // Who knows if this directory will somewhen cease to exist...
                .CreateSubdirectory("CitizenMP.Server")
                .CreateSubdirectory("data");
            var outputDirectory = new DirectoryInfo(options.OutputPath);
            var binOutputDirectory = new DirectoryInfo(Path.Combine(outputDirectory.FullName, "bin"));

            // Do we even have a copy or do we need to clone?
            if (!Repository.IsValid(sourceDirectory.FullName))
            {
                if (sourceDirectory.Exists)
                {
                    Console.WriteLine("Deleting source code folder...");
                    sourceDirectory.Delete(true);
                }

                Console.WriteLine("Cloning source code repository...");
                Repository.Clone("http://tohjo.ez.lv/citidev/citizenmp-server.git", sourceDirectory.FullName);
            }
            else
            {
                // Update working dir
                Console.WriteLine("Updating source code...");
                using (var git = new Repository(sourceDirectory.FullName))
                {
                    //git.Network.Pull(GitSignature, new PullOptions());
                    git.UpdateRepository("HEAD");
                }
            }

            // Check if we need to update by parsing AssemblyConfigurationAttribute in server assembly.
            // Should have a space-separated segment saying "CommitHash=<commit hash here>".
            if (binOutputDirectory.Exists)
            {
                var serverBins = binOutputDirectory
                    .EnumerateFiles("*Server.exe", SearchOption.TopDirectoryOnly)
                    .ToArray();
                if (serverBins.Any())
                {
                    var serverAssembly = Assembly.LoadFile(serverBins.First().FullName);
                    var configurationAttribs =
                        serverAssembly.GetCustomAttributes(typeof (AssemblyConfigurationAttribute), false);
                    if (configurationAttribs.Any())
                    {
                        var configurationAttrib = (AssemblyConfigurationAttribute) configurationAttribs.First();
                        foreach (var commitHash in configurationAttrib.Configuration.Split(' ')
                            .Where(section => section.StartsWith("CommitHash="))
                            .Select(section => section.Split('=').Last()))
                        {
                            using (var repo = new Repository(sourceDirectory.FullName))
                            {
                                if (commitHash != repo.Head.Tip.Sha)
                                    continue;

                                // Yup, same commit.
                                Console.WriteLine("Server is already up-to-date!");
                                return 0;
                            }
                        }
                    }
                }
            }

            // Get submodules
            using (var git = new Repository(sourceDirectory.FullName))
            {
                Console.WriteLine("Downloading dependencies...");
                git.UpdateSubmodules();
            }

            // Patch AssemblyInfo.cs to include commit hash in an AssemblyConfigurationAttribute
            Console.WriteLine("Patching assembly information...");
            var assemblyGuidRegex =
                new Regex(
                    @"^[\s]*\[assembly[\s]*:[\s]*Guid[\s]*\([\s]*(?<verbatimPrefix>[@]?)""(?<oldValue>.*?)""[\s]*\)[\s]*\][\s]*$",
                    RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline);
            var assemblyConfigurationRegex =
                new Regex(
                    @"^[\s]*\[assembly[\s]*:[\s]*AssemblyConfiguration[\s]*\([\s]*(?<verbatimPrefix>[@]?)""(?<oldValue>.*?)""[\s]*\)[\s]*\][\s]*$",
                    RegexOptions.Compiled | RegexOptions.Singleline | RegexOptions.Multiline);
            foreach (var assemblyInfoFile in sourceDirectory
                .EnumerateFiles("AssemblyInfo.cs", SearchOption.AllDirectories))
            {
                var sourceCode = File.ReadAllText(assemblyInfoFile.FullName);

                // Parse GUID
                var guid = assemblyGuidRegex.Match(sourceCode).Groups["oldValue"].Value;
                if (!guid.Equals("b14ff4c2-a2e5-416b-ae79-4580cda4d9d1", StringComparison.OrdinalIgnoreCase))
                {
                    //Console.WriteLine("\tSkipping assembly info for GUID \"{0}\" ({1}).", guid, assemblyInfoFile.Directory);
                    continue;
                }
                //Console.WriteLine("\tPatching assembly info for GUID \"{0}\" ({1}).", guid, assemblyInfoFile.Directory);

                if (!assemblyConfigurationRegex.IsMatch(sourceCode))
                {
                    sourceCode += Environment.NewLine;
                    sourceCode += @"// Inserted by CitizenMP Server Updater for version comparison";
                    sourceCode += @"[assembly: AssemblyConfiguration("""")]";
                }

                using (var git = new Repository(sourceDirectory.FullName))
                {
                    sourceCode = assemblyConfigurationRegex.Replace(sourceCode,
                        m => string.Format("[assembly: AssemblyConfiguration({0}\"{1}CommitHash={2}\")]",
                            m.Groups["verbatimPrefix"].Value,
                            m.Groups["oldValue"].Length > 0
                                ? m.Groups["oldValue"].Value + " "
                                : "",
                            // ReSharper disable once AccessToDisposedClosure
                            git.Head.Tip.Sha));
                }

                File.WriteAllText(assemblyInfoFile.FullName, sourceCode);
            }


            // Build project
            //Console.WriteLine("Building server binaries...");
            var slnPath = sourceDirectory.EnumerateFiles("*.sln", SearchOption.TopDirectoryOnly)
                .First().FullName;
            outputDirectory.Create();
            var logpath = options.WriteLogFile ? Path.Combine(outputDirectory.FullName, "build.log") : null;
            if (!Build(slnPath, new Dictionary<string, string>
            {
                {"Configuration", "Release"},
                {"Platform", "Any CPU"},
                {"DebugType", IsWin32() ? "None" : "pdbonly"},
                {"DebugSymbols", false.ToString()},
                {"OutputPath", binOutputDirectory.FullName},
                {"AllowedReferenceRelatedFileExtensions", "\".mdb\"=\"\";\".pdb\"=\"\";\".xml\"=\"\""}
            }, logpath))
            {
                Console.Error.WriteLine("Build failed! Please look at {0} for more information.", logpath);
                return 1;
            }

            // Prepare with default files
            PrepareDirectory(dataSourceDirectory, outputDirectory);

            // Write startup scripts
            switch (Environment.OSVersion.Platform)
            {
                case PlatformID.Unix:
                case PlatformID.MacOSX:
                {
                    var startScriptPath = Path.Combine(outputDirectory.FullName, "start.sh");
                    File.WriteAllText(
                        startScriptPath,
                        string.Join(
                            Environment.NewLine,
                            @"#!/bin/bash",
                            @"",
                            @"# switch to the script directory",
                            @"cd ""$( dirname ""${BASH_SOURCE[0]}"" )""",
                            @"",
                            @"# run with mono",
                            @"mono ""bin/" + binOutputDirectory.EnumerateFiles("*.exe").First().Name + @""" $@",
                            @""));

                    // TODO: Pretty sure there is an easier way to do a programmatical chmod +x
                    Stat stat;
                    FilePermissions perms;
                    if (UnixSyscall.stat(startScriptPath, out stat) != 0)
                    {
                        perms = FilePermissions.S_IRUSR | FilePermissions.S_IRGRP | FilePermissions.S_IROTH
                                | FilePermissions.S_IWUSR
                                | FilePermissions.S_IXUSR;
                    }
                    else
                    {
                        perms = stat.st_mode;
                    }
                    UnixSyscall.chmod(startScriptPath,
                        perms
                        | FilePermissions.S_IXUSR | FilePermissions.S_IXGRP | FilePermissions.S_IXOTH);
                }
                    break;
                case PlatformID.Win32NT:
                case PlatformID.Win32Windows:
                {
                    var startScriptPath = Path.Combine(outputDirectory.FullName, "start.bat");
                    File.WriteAllText(
                        startScriptPath,
                        string.Join(Environment.NewLine,
                            "@echo off",
                            @"",
                            @":: switch to the script directory",
                            @"pushd ""%~dp0""",
                            @"",
                            @":: run",
                            @"""bin\" + binOutputDirectory.EnumerateFiles("*.exe").First().Name + @""" %*",
                            @""));
                }
                    break;
                default:
                    Console.Error.WriteLine("WARNING: No startup script created. Platform not supported.");
                    break;
            }

            Console.WriteLine("Done.");
            return 0;
        }

        private static bool Build(string projectFilePath, IDictionary<string, string> buildProperties,
            string logPath = null)
        {
            var workspace = new FileInfo(projectFilePath).Directory;
            if (workspace == null)
                throw new DirectoryNotFoundException(
                    "Somehow the project file is not in a directory. Report this to Icedream!");

            // Mono compatibility
            Environment.SetEnvironmentVariable("MONO_IOMAP", "all");

            // Make sure we restore nuget packages automatically and without permissions interfering with /tmp/nuget/ (edge case)
            var newTempDir = workspace.CreateSubdirectory(".tmp");
            Environment.SetEnvironmentVariable("EnableNuGetPackageRestore", "true");
            Environment.SetEnvironmentVariable("TEMP", newTempDir.FullName);
            Environment.SetEnvironmentVariable("TMP", newTempDir.FullName);

            try
            {
                var pc = new ProjectCollection();
                pc.RegisterLogger(new ConsoleLogger(LoggerVerbosity.Minimal));

                var loggers = new List<ILogger>();
                if (logPath != null)
                    loggers.Add(new FileLogger
                    {
                        Parameters = string.Format("LogFile={0}", logPath),
                        Verbosity = LoggerVerbosity.Detailed,
                        ShowSummary = true,
                        SkipProjectStartedText = true
                    });
                loggers.Add(new ConsoleLogger(LoggerVerbosity.Quiet) {ShowSummary = false});

                // Import/Update Mozilla certs for NuGet to not fail out on non-Windows machines
                if (!IsWin32())
                {
                    try
                    {
                        var automaticYesResponder = new Action<string, StreamWriter>((s, writer) =>
                        {
                            if (s == null || !s.Contains("X.509 Certificate"))
                                return;
                            writer.WriteLine("y");
                        });

                        // TODO: Make sure this does not fail out by checking if mozroots is installed
                        Console.WriteLine("Updating SSL certificates for NuGet...");
                        Run("mozroots", "--import --sync --quiet");
                        Run("certmgr", "-ssl https://go.microsoft.com", automaticYesResponder);
                        Run("certmgr", "-ssl https://nugetgallery.blob.core.windows.net", automaticYesResponder);
                        Run("certmgr", "-ssl https://nuget.org", automaticYesResponder);
                    }
                    catch (Exception error)
                    {
                        Console.Error.WriteLine("ERROR: {0}", error.Message);
                        throw;
                    }
                }

                {
                    // The NuGet.exe that is shipped with CitizenMP.Server has a few bugs...
                    var nugetExePath = Path.Combine(workspace.FullName, ".nuget", "NuGet.exe");
                    Console.WriteLine("Updating NuGet...");
                    File.Delete(nugetExePath);
                    using (var wc = new WebClient())
                    {
                        wc.DownloadFile("https://nuget.org/NuGet.exe", nugetExePath);
                    }
                }

                // Use a different build route if running on the Mono interpreter
                if (!IsWin32())
                {
                    // Build doesn't work with the new API on Mono, use the deprecated api
                    Console.WriteLine("Building server binaries...");
#pragma warning disable 618
                    foreach (var logger in loggers)
                        Engine.GlobalEngine.RegisterLogger(logger);
                    var project = new Project(Engine.GlobalEngine) {BuildEnabled = true};
                    project.Load(projectFilePath);
                    foreach (var property in buildProperties)
                        project.GlobalProperties.SetProperty(property.Key, property.Value);
                    var result = project.Build();
#pragma warning restore 618
                    return result;
                }

                // Windows build can make use of the new API which is more efficient
                {
                    Console.WriteLine("Building server binaries...");

                    var buildReq = new BuildRequestData(projectFilePath, buildProperties, null, new[] {"Build"}, null);

                    var result = BuildManager.DefaultBuildManager.Build(
                        new BuildParameters(pc)
                        {
                            Loggers = loggers.ToArray(),
                            MaxNodeCount = Environment.ProcessorCount
                        }, buildReq);

                    return result.OverallResult == BuildResultCode.Success;
                }
            }
            finally
            {
                newTempDir.Delete(true);
            }
        }

        private static void Run(string name, string args, Action<string, StreamWriter> lineProcessor = null)
        {
            using (var p = Process.Start(new ProcessStartInfo {
                    Arguments = args,
                    FileName = name,
                    UseShellExecute = false,
                    CreateNoWindow = true,
                    RedirectStandardInput = lineProcessor != null,
                    RedirectStandardOutput = lineProcessor != null
                }))
            {
                if (lineProcessor == null)
                {
                    p.WaitForExit();
                }
                else
                {
                    while (!p.HasExited)
                    {
                        string line = p.StandardOutput.ReadLine();
                        lineProcessor.Invoke(line, p.StandardInput);
                    }
                }

                if (p.ExitCode != 0)
                {
                    throw new Exception(string.Format("Process \"{0} {1}\" exited with error code {2}",
                        name, args, p.ExitCode));
                }
            }
        }

        private static bool IsWin32()
        {
            return Environment.OSVersion.Platform == PlatformID.Win32NT ||
                   Environment.OSVersion.Platform == PlatformID.Win32S ||
                   Environment.OSVersion.Platform == PlatformID.Win32Windows;
        }
    }
}