using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using Microsoft.Win32;
using Packages.Rider.Editor.Util;
using Unity.CodeEditor;
using UnityEngine;

namespace Packages.Rider.Editor
{
  internal interface IDiscovery
  {
    CodeEditor.Installation[] PathCallback();
  }

  internal class Discovery : IDiscovery
  {
    public CodeEditor.Installation[] PathCallback()
    {
      var res = RiderPathLocator.GetAllRiderPaths()
        .Select(riderInfo => new CodeEditor.Installation
        {
          Path = riderInfo.Path,
          Name = riderInfo.Presentation
        })
        .ToList();

      var editorPath = RiderScriptEditor.CurrentEditor;
      if (RiderScriptEditor.IsRiderOrFleetInstallation(editorPath) &&
          !res.Any(a => a.Path == editorPath) &&
          FileSystemUtil.EditorPathExists(editorPath))
      {
        // External editor manually set from custom location
        var info = new RiderPathLocator.RiderInfo(editorPath, false);
        var installation = new CodeEditor.Installation
        {
          Path = info.Path,
          Name = info.Presentation
        };
        res.Add(installation);
      }

      return res.ToArray();
    }
  }

  /// <summary>
  /// This code is a modified version of the JetBrains resharper-unity plugin listed here:
  /// https://github.com/JetBrains/resharper-unity/blob/master/unity/JetBrains.Rider.Unity.Editor/EditorPlugin/RiderPathLocator.cs
  /// </summary>
  internal static class RiderPathLocator
  {
#if !(UNITY_4_7 || UNITY_5_5)
    public static RiderInfo[] GetAllRiderPaths()
    {
      try
      {
        switch (SystemInfo.operatingSystemFamily)
        {
          case OperatingSystemFamily.Windows:
          {
            return CollectRiderInfosWindows();
          }

          case OperatingSystemFamily.MacOSX:
          {
            return CollectRiderInfosMac();
          }

          case OperatingSystemFamily.Linux:
          {
            return CollectAllRiderPathsLinux();
          }
        }
      }
      catch (Exception e)
      {
        Debug.LogException(e);
      }

      return Array.Empty<RiderInfo>();
    }
#endif
    private static RiderInfo[] CollectAllRiderPathsLinux()
    {
      var installInfos = new List<RiderInfo>();
      var appsPath = GetAppsRootPathInToolbox();
      
      var riderRootPath = Path.Combine(appsPath, "Rider");
      installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider.sh", false)
        .Select(a => new RiderInfo(a, true)).ToList());
      
      var fleetRootPath = Path.Combine(appsPath, "Fleet");
      installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "bin", "Fleet", false)
        .Select(a => new RiderInfo(a, true)).ToList());
      
      var home = Environment.GetEnvironmentVariable("HOME"); 
      if (!string.IsNullOrEmpty(home)) 
      {
        //$Home/.local/share/applications/jetbrains-rider.desktop
        var shortcut = new FileInfo(Path.Combine(home, @".local/share/applications/jetbrains-rider.desktop"));

        if (shortcut.Exists)
        {
          var lines = File.ReadAllLines(shortcut.FullName);
          foreach (var line in lines)
          {
            if (!line.StartsWith("Exec=\""))
              continue;
            var path = line.Split('"').Where((_, index) => index == 1).SingleOrDefault();
            if (string.IsNullOrEmpty(path))
              continue;

            if (installInfos.Any(a => a.Path == path)) // avoid adding similar build as from toolbox
              continue;
            installInfos.Add(new RiderInfo(path, false));
          }
        }
      }

      // snap install
      var snapInstallPath = "/snap/rider/current/bin/rider.sh";
      if (new FileInfo(snapInstallPath).Exists)
        installInfos.Add(new RiderInfo(snapInstallPath, false));
      
      return installInfos.ToArray();
    }

    private static RiderInfo[] CollectRiderInfosMac()
    {
      var installInfos = new List<RiderInfo>();
      // "/Applications/*Rider*.app"
      var folder = new DirectoryInfo("/Applications");
      if (folder.Exists)
      {
        installInfos.AddRange(folder.GetDirectories("*Rider*.app")
          .Select(a => new RiderInfo(a.FullName, false))
          .ToList());
      }

      var appsPath = GetAppsRootPathInToolbox();
      var riderRootPath = Path.Combine(appsPath, "Rider");
      installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "", "Rider*.app", true)
        .Select(a => new RiderInfo(a, true)));
      
      var fleetRootPath = Path.Combine(appsPath, "Fleet");
      installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, "", "Fleet*.app", true)
        .Select(a => new RiderInfo(a, true)));

      return installInfos.ToArray();
    }

    private static RiderInfo[] CollectRiderInfosWindows()
    {
      var appsPath = GetAppsRootPathInToolbox();
      var installInfos = new List<RiderInfo>();
      var riderRootPath = Path.Combine(appsPath, "Rider");
      installInfos.AddRange(CollectPathsFromToolbox(riderRootPath, "bin", "rider64.exe", false).ToList()
        .Select(a => new RiderInfo(a, true)).ToList());
      
      var fleetRootPath = Path.Combine(appsPath, "Fleet");
      installInfos.AddRange(CollectPathsFromToolbox(fleetRootPath, string.Empty, "Fleet.exe", false).ToList()
        .Select(a => new RiderInfo(a, true)).ToList());
      
      var installPaths = new List<string>();
      const string registryKey = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall";
      CollectPathsFromRegistry(registryKey, installPaths);
      const string wowRegistryKey = @"SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall";
      CollectPathsFromRegistry(wowRegistryKey, installPaths);
      
      installInfos.AddRange(installPaths.Select(a => new RiderInfo(a, false)).ToList());

      return installInfos.ToArray();
    }

    private static string GetAppsRootPathInToolbox()
    {
      string localAppData = string.Empty;
      switch (SystemInfo.operatingSystemFamily)
      {
        case OperatingSystemFamily.Windows:
        {
          localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
          break;
        }

        case OperatingSystemFamily.MacOSX:
        {
          var home = Environment.GetEnvironmentVariable("HOME");
          if (!string.IsNullOrEmpty(home))
          {
            localAppData = Path.Combine(home, @"Library/Application Support");
          }
          break;
        }

        case OperatingSystemFamily.Linux:
        {
          var home = Environment.GetEnvironmentVariable("HOME");
          if (!string.IsNullOrEmpty(home))
          {
            localAppData = Path.Combine(home, @".local/share");
          }
          break;
        }
        default:
          throw new Exception("Unknown OS");
      }
      
      var toolboxPath = Path.Combine(localAppData, @"JetBrains/Toolbox");
      var settingsJson = Path.Combine(toolboxPath, ".settings.json");

      if (File.Exists(settingsJson))
      {
        var path = SettingsJson.GetInstallLocationFromJson(File.ReadAllText(settingsJson));
        if (!string.IsNullOrEmpty(path))
          toolboxPath = path;
      }

      return Path.Combine(toolboxPath, "apps");
    }

    internal static ProductInfo GetBuildVersion(string path)
    {
      var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
      var dir = buildTxtFileInfo.DirectoryName;
      if (!Directory.Exists(dir))
        return null;
      var buildVersionFile = new FileInfo(Path.Combine(dir, "product-info.json"));
      if (!buildVersionFile.Exists) 
        return null;
      var json = File.ReadAllText(buildVersionFile.FullName);
      return ProductInfo.GetProductInfo(json);
    }
    
    internal static Version GetBuildNumber(string path)
    {
      var buildTxtFileInfo = new FileInfo(Path.Combine(path, GetRelativePathToBuildTxt()));
      return GetBuildNumberWithBuildTxt(buildTxtFileInfo) ?? GetBuildNumberFromPath(path);
    }
    
    private static Version GetBuildNumberWithBuildTxt(FileInfo file)
    {
      if (!file.Exists) 
        return null;
      var text = File.ReadAllText(file.FullName);
      var index = text.IndexOf("-", StringComparison.Ordinal) + 1; // RD-191.7141.355
      if (index <= 0) 
        return null;
      
      var versionText = text.Substring(index);
      return Version.TryParse(versionText, out var v) ? v : null;
    }

    [CanBeNull]
    private static Version GetBuildNumberFromPath(string input)
    {
      if (string.IsNullOrEmpty(input))
        return null;

      var match = Regex.Match(input, @"(?<major>\d+)\.(?<minor>\d+)\.(?<build>\d+)");
      var groups = match.Groups;
      Version version = null;
      if (match.Success)
      {
        version = Version.Parse($"{groups["major"].Value}.{groups["minor"].Value}.{groups["build"].Value}");
      }

      return version;
    }

    public static bool GetIsToolbox(string path)
    {
      return Path.GetFullPath(path).StartsWith(Path.GetFullPath(GetAppsRootPathInToolbox()));
    }

    private static string GetRelativePathToBuildTxt()
    {
      switch (SystemInfo.operatingSystemFamily)
      {
        case OperatingSystemFamily.Windows: 
        case OperatingSystemFamily.Linux:
          return "../../build.txt";
        case OperatingSystemFamily.MacOSX:
          return "Contents/Resources/build.txt";
      }
      throw new Exception("Unknown OS");
    }
    private static void CollectPathsFromRegistry(string registryKey, List<string> installPaths)
    {
      using (var key = Registry.CurrentUser.OpenSubKey(registryKey))
      {
        CollectPathsFromRegistry(installPaths, key);
      }
      using (var key = Registry.LocalMachine.OpenSubKey(registryKey))
      {
        CollectPathsFromRegistry(installPaths, key);
      }
    }

    private static void CollectPathsFromRegistry(List<string> installPaths, RegistryKey key)
    {
      if (key == null) return;
      foreach (var subkeyName in key.GetSubKeyNames())
      {
        using (var subkey = key.OpenSubKey(subkeyName))
        {
          var folderObject = subkey?.GetValue("InstallLocation");
          if (folderObject == null) continue;
          var folder = folderObject.ToString();
          if (folder.Length == 0) continue;
          var displayName = subkey.GetValue("DisplayName");
          if (displayName == null) continue;
          if (!displayName.ToString().Contains("Rider")) continue;
          try // possible "illegal characters in path"
          {
            var possiblePath = Path.Combine(folder, @"bin\rider64.exe"); 
            if (File.Exists(possiblePath))
              installPaths.Add(possiblePath);
          }
          catch (ArgumentException) { }
        }
      }
    }

    private static string[] CollectPathsFromToolbox(string productRootPathInToolbox, string dirName, string searchPattern,
      bool isMac)
    {
      if (!Directory.Exists(productRootPathInToolbox))
        return new string[0];

      var channelDirs = Directory.GetDirectories(productRootPathInToolbox);
      var paths = channelDirs.SelectMany(channelDir =>
        {
          try
          {
            // use history.json - last entry stands for the active build https://jetbrains.slack.com/archives/C07KNP99D/p1547807024066500?thread_ts=1547731708.057700&cid=C07KNP99D
            var historyFile = Path.Combine(channelDir, ".history.json");
            if (File.Exists(historyFile))
            {
              var json = File.ReadAllText(historyFile);
              var build = ToolboxHistory.GetLatestBuildFromJson(json);
              if (build != null)
              {
                var buildDir = Path.Combine(channelDir, build);
                var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
                if (executablePaths.Any())
                  return executablePaths;
              }
            }

            var channelFile = Path.Combine(channelDir, ".channel.settings.json");
            if (File.Exists(channelFile))
            {
              var json = File.ReadAllText(channelFile).Replace("active-application", "active_application");
              var build = ToolboxInstallData.GetLatestBuildFromJson(json);
              if (build != null)
              {
                var buildDir = Path.Combine(channelDir, build);
                var executablePaths = GetExecutablePaths(dirName, searchPattern, isMac, buildDir);
                if (executablePaths.Any())
                  return executablePaths;
              }
            }

            // changes in toolbox json files format may brake the logic above, so return all found installations
            return Directory.GetDirectories(channelDir)
              .SelectMany(buildDir => GetExecutablePaths(dirName, searchPattern, isMac, buildDir));
          }
          catch (Exception e)
          {
            // do not write to Debug.Log, just log it.
            Logger.Warn($"Failed to get path from {channelDir}", e);
          }

          return new string[0];
        })
        .Where(c => !string.IsNullOrEmpty(c))
        .ToArray();
      return paths;
    }

    private static string[] GetExecutablePaths(string dirName, string searchPattern, bool isMac, string buildDir)
    {
      var folder = new DirectoryInfo(Path.Combine(buildDir, dirName));
      if (!folder.Exists)
        return new string[0];

      if (!isMac)
        return new[] {Path.Combine(folder.FullName, searchPattern)}.Where(File.Exists).ToArray();
      return folder.GetDirectories(searchPattern).Select(f => f.FullName)
        .Where(Directory.Exists).ToArray();
    }

    // Disable the "field is never assigned" compiler warning. We never assign it, but Unity does.
    // Note that Unity disable this warning in the generated C# projects
#pragma warning disable 0649
    
    [Serializable]
    class SettingsJson
    {
      // ReSharper disable once InconsistentNaming
      public string install_location;
      
      [CanBeNull]
      public static string GetInstallLocationFromJson(string json)
      {
        try
        {
#if UNITY_4_7 || UNITY_5_5
          return JsonConvert.DeserializeObject<SettingsJson>(json).install_location;
#else
          return JsonUtility.FromJson<SettingsJson>(json).install_location;
#endif
        }
        catch (Exception)
        {
          Logger.Warn($"Failed to get install_location from json {json}");
        }

        return null;
      }
    }

    [Serializable]
    class ToolboxHistory
    {
      public List<ItemNode> history;

      [CanBeNull]
      public static string GetLatestBuildFromJson(string json)
      {
        try
        {
#if UNITY_4_7 || UNITY_5_5
          return JsonConvert.DeserializeObject<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
#else
          return JsonUtility.FromJson<ToolboxHistory>(json).history.LastOrDefault()?.item.build;
#endif
        }
        catch (Exception)
        {
          Logger.Warn($"Failed to get latest build from json {json}");
        }

        return null;
      }
    }

    [Serializable]
    class ItemNode
    {
      public BuildNode item;
    }

    [Serializable]
    class BuildNode
    {
      public string build;
    }

    [Serializable]
    internal class ProductInfo
    {
      public string version;
      public string versionSuffix;

      [CanBeNull]
      internal static ProductInfo GetProductInfo(string json)
      {
        try
        {
          var productInfo = JsonUtility.FromJson<ProductInfo>(json);
          return productInfo;
        }
        catch (Exception)
        {
          Logger.Warn($"Failed to get version from json {json}");
        }

        return null;
      }
    }

    // ReSharper disable once ClassNeverInstantiated.Global
    [Serializable]
    class ToolboxInstallData
    {
      // ReSharper disable once InconsistentNaming
      public ActiveApplication active_application;

      [CanBeNull]
      public static string GetLatestBuildFromJson(string json)
      {
        try
        {
#if UNITY_4_7 || UNITY_5_5
          var toolbox = JsonConvert.DeserializeObject<ToolboxInstallData>(json);
#else
          var toolbox = JsonUtility.FromJson<ToolboxInstallData>(json);
#endif
          var builds = toolbox.active_application.builds;
          if (builds != null && builds.Any())
            return builds.First();
        }
        catch (Exception)
        {
          Logger.Warn($"Failed to get latest build from json {json}");
        }

        return null;
      }
    }

    [Serializable]
    class ActiveApplication
    {
      // ReSharper disable once InconsistentNaming
      public List<string> builds;
    }

#pragma warning restore 0649

    internal struct RiderInfo
    {
      public bool IsToolbox;
      public string Presentation;
      public Version BuildNumber;
      public ProductInfo ProductInfo;
      public string Path;

      public RiderInfo(string path, bool isToolbox)
      {
        if (path == RiderScriptEditor.CurrentEditor)
        {
          RiderScriptEditorData.instance.Init();
          BuildNumber = RiderScriptEditorData.instance.editorBuildNumber.ToVersion();
          ProductInfo = RiderScriptEditorData.instance.productInfo;
        }
        else
        {
          BuildNumber = GetBuildNumber(path);
          ProductInfo = GetBuildVersion(path);
        }

        var fileInfo = new FileInfo(path);
        var productName = RiderScriptEditor.GetProductNameForPresentation(fileInfo);  
        Path = fileInfo.FullName; // normalize separators
        var presentation = $"{productName} {BuildNumber}";

        if (ProductInfo != null && !string.IsNullOrEmpty(ProductInfo.version))
        {
          var suffix = string.IsNullOrEmpty(ProductInfo.versionSuffix) ? "" : $" {ProductInfo.versionSuffix}";
          presentation = $"{productName} {ProductInfo.version}{suffix}";
        }

        if (isToolbox)
          presentation += " (JetBrains Toolbox)";

        Presentation = presentation;
        IsToolbox = isToolbox;
      }
    }

    private static class Logger
    {
      internal static void Warn(string message, Exception e = null)
      {
#if RIDER_EDITOR_PLUGIN // can't be used in com.unity.ide.rider
        Log.GetLog(typeof(RiderPathLocator).Name).Warn(message);
        if (e != null) 
          Log.GetLog(typeof(RiderPathLocator).Name).Warn(e);
#else
        Debug.LogError(message);
        if (e != null)
          Debug.LogException(e);
#endif
      }
    }
  }
}