#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Microsoft.Win32;
using TrustedUninstaller.Shared.Exceptions;
using TrustedUninstaller.Shared.Tasks;
using YamlDotNet.Serialization;


namespace TrustedUninstaller.Shared.Actions
{
    public enum RegistryValueOperation
    {
        Delete = 0,
        Add = 1,
        // This indicates to skip the action if the specified value does not already exist
        Set = 2
    }

    public enum RegistryValueType
    {
        REG_SZ = RegistryValueKind.String,
        REG_MULTI_SZ = RegistryValueKind.MultiString,
        REG_EXPAND_SZ = RegistryValueKind.ExpandString,
        REG_DWORD = RegistryValueKind.DWord,
        REG_QWORD = RegistryValueKind.QWord,
        REG_BINARY = RegistryValueKind.Binary,
        REG_NONE = RegistryValueKind.None,
        REG_UNKNOWN = RegistryValueKind.Unknown
    }

    public class RegistryValueAction : ITaskAction
    {
        [YamlMember(typeof(string), Alias = "path")]
        public string KeyName { get; set; }

        [YamlMember(typeof(string), Alias = "value")]
        public string Value { get; set; } = "";
        
        [YamlMember(typeof(object), Alias = "data")]
        public object? Data { get; set; }
        
        [YamlMember(typeof(RegistryValueType), Alias = "type")]
        public RegistryValueType Type { get; set; }
        
        [YamlMember(typeof(Scope), Alias = "scope")]
        public Scope Scope { get; set; } = Scope.AllUsers;

        [YamlMember(typeof(RegistryValueOperation), Alias = "operation")]
        public RegistryValueOperation Operation { get; set; } = RegistryValueOperation.Add;
        
        [YamlMember(typeof(string), Alias = "weight")]
        public int ProgressWeight { get; set; } = 1;
        public int GetProgressWeight()
        {
            /*
            int roots;
            try
            {
                roots = GetRoots().Count;
            }
            catch (Exception e)
            {

                roots = 1;
            }
            */

            return ProgressWeight;
        }
        
        private bool InProgress { get; set; }
        public void ResetProgress() => InProgress = false;
        
        public string ErrorString() => $"RegistryValueAction failed to {Operation.ToString().ToLower()} value '{Value}' in key '{KeyName}'";
        
        private List<RegistryKey> GetRoots()
        {
            var hive = KeyName.Split('\\').GetValue(0).ToString().ToUpper();
            var list = new List<RegistryKey>();

            if (hive.Equals("HKCU") || hive.Equals("HKEY_CURRENT_USER"))
            {
                RegistryKey usersKey;
                List<string> userKeys;

                switch (Scope)
                {
                    case Scope.AllUsers:
                        WinUtil.RegistryManager.HookUserHives();
                    
                        usersKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default);
                        userKeys = usersKey.GetSubKeyNames().
                            Where(x => x.StartsWith("S-") && 
                                usersKey.OpenSubKey(x).GetSubKeyNames().Any(y => y.Equals("Volatile Environment"))).ToList();
                    
                        userKeys.AddRange(usersKey.GetSubKeyNames().Where(x => x.StartsWith("AME_UserHive_") && !x.EndsWith("_Classes")).ToList());
                    
                        userKeys.ForEach(x => list.Add(usersKey.OpenSubKey(x, true)));
                        return list;
                    case Scope.ActiveUsers:
                        usersKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default);
                        userKeys = usersKey.GetSubKeyNames().
                            Where(x => x.StartsWith("S-") && 
                                usersKey.OpenSubKey(x).GetSubKeyNames().Any(y => y.Equals("Volatile Environment"))).ToList();

                        userKeys.ForEach(x => list.Add(usersKey.OpenSubKey(x, true)));
                        return list;
                    case Scope.DefaultUser:
                        usersKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default);
                        userKeys = usersKey.GetSubKeyNames().Where(x => x.Equals("AME_UserHive_Default") && !x.EndsWith("_Classes")).ToList();
                        
                        userKeys.ForEach(x => list.Add(usersKey.OpenSubKey(x, true)));
                        return list;
                }
            }
            list.Add(hive switch
            {
                "HKCU" => RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default),
                "HKEY_CURRENT_USER" => RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default),
                "HKLM" => RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default),
                "HKEY_LOCAL_MACHINE" => RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Default),
                "HKCR" => RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Default),
                "HKEY_CLASSES_ROOT" => RegistryKey.OpenBaseKey(RegistryHive.ClassesRoot, RegistryView.Default),
                "HKU" => RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default),
                "HKEY_USERS" => RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default),
                _ => throw new ArgumentException($"Key '{KeyName}' does not specify a valid registry hive.")
            });
            return list;
        }

        public string GetSubKey() => KeyName.Substring(KeyName.IndexOf('\\') + 1);

        private RegistryKey? OpenSubKey(RegistryKey root)
        {
            var subKeyPath = GetSubKey();
            
            if (subKeyPath == null) throw new ArgumentException($"Key '{KeyName}' is invalid.");
            
            return root.OpenSubKey(subKeyPath, true);
        }

        public object? GetCurrentValue(RegistryKey root)
        {
            var subkey = GetSubKey();
            return Registry.GetValue(root.Name + "\\" + subkey, Value, null);
        }
        
        public static byte[] StringToByteArray(string hex) {
            return Enumerable.Range(0, hex.Length)
                .Where(x => x % 2 == 0)
                .Select(x => Convert.ToByte(hex.Substring(x, 2), 16))
                .ToArray();
        }

        public UninstallTaskStatus GetStatus()
        {
            try
            {
                var roots = GetRoots();

                foreach (var _root in roots)
                {
                    var root = _root;
                    var subKey = GetSubKey();
                    
                    if (root.Name.Contains("AME_UserHive_") && subKey.StartsWith("SOFTWARE\\Classes", StringComparison.CurrentCultureIgnoreCase))
                    {
                        var usersKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default);

                        root = usersKey.OpenSubKey(root.Name.Substring(11) + "_Classes", true);
                        subKey = Regex.Replace(subKey, @"^SOFTWARE\\*Classes\\*", "", RegexOptions.IgnoreCase);

                        if (root == null)
                        {
                            continue;
                        }
                    }
                    
                    var openedSubKey = root.OpenSubKey(subKey);

                    if (openedSubKey == null && (Operation == RegistryValueOperation.Set || Operation == RegistryValueOperation.Delete))
                        continue;
                    if (openedSubKey == null) return UninstallTaskStatus.ToDo;

                    var value = openedSubKey.GetValue(Value);

                    if (value == null)
                    {
                        if (Operation == RegistryValueOperation.Set || Operation == RegistryValueOperation.Delete)
                            continue;

                        return UninstallTaskStatus.ToDo;
                    }
                    if (Operation == RegistryValueOperation.Delete) return UninstallTaskStatus.ToDo;

                    if (Data == null) return UninstallTaskStatus.ToDo;


                    bool matches;
                    try
                    {
                        matches = Type switch
                        {
                            RegistryValueType.REG_SZ =>
                                Data.ToString() == value.ToString(),
                            RegistryValueType.REG_EXPAND_SZ =>
                                // RegistryValueOptions.DoNotExpandEnvironmentNames above did not seem to work.
                                Environment.ExpandEnvironmentVariables(Data.ToString()) == value.ToString(),
                            RegistryValueType.REG_MULTI_SZ =>
                                Data.ToString() == "" ?
                                    ((string[])value).SequenceEqual(new string[] { }) :
                                    ((string[])value).SequenceEqual(Data.ToString().Split(new string[] { "\\0" }, StringSplitOptions.None)),
                            RegistryValueType.REG_DWORD =>
                                unchecked((int)Convert.ToUInt32(Data)) == (int)value,
                            RegistryValueType.REG_QWORD =>
                                Convert.ToUInt64(Data) == (ulong)value,
                            RegistryValueType.REG_BINARY =>
                                ((byte[])value).SequenceEqual(StringToByteArray(Data.ToString())),
                            RegistryValueType.REG_NONE =>
                                ((byte[])value).SequenceEqual(new byte[0]),
                            RegistryValueType.REG_UNKNOWN =>
                                Data.ToString() == value.ToString(),
                            _ => throw new ArgumentException("Impossible.")
                        };
                    }
                    catch (InvalidCastException)
                    {
                        matches = false;
                    }
                    if (!matches) return UninstallTaskStatus.ToDo;
                }
            }
            catch (Exception e)
            {
                ErrorLogger.WriteToErrorLog(e.Message,
                    e.StackTrace, "RegistryValueAction Error");
                return UninstallTaskStatus.ToDo;
            }

            return UninstallTaskStatus.Completed;
        }

        public async Task<bool> RunTask()
        {

            Console.WriteLine($"{Operation.ToString().TrimEnd('e')}ing value '{Value}' in key '{KeyName}'...");
            
            var roots = GetRoots();

            foreach (var _root in roots)
            {
                var root = _root;
                var subKey = GetSubKey();
                try
                {
                    if (root.Name.Contains("AME_UserHive_") && subKey.StartsWith("SOFTWARE\\Classes", StringComparison.CurrentCultureIgnoreCase))
                    {
                        var usersKey = RegistryKey.OpenBaseKey(RegistryHive.Users, RegistryView.Default);

                        root = usersKey.OpenSubKey(root.Name.Substring(11) + "_Classes", true);
                        subKey = Regex.Replace(subKey, @"^SOFTWARE\\*Classes\\*", "", RegexOptions.IgnoreCase);

                        if (root == null)
                        {
                            ErrorLogger.WriteToErrorLog($"User classes hive not found for hive {_root.Name}.",
                                Environment.StackTrace, "RegistryValueAction Error");
                            continue;
                        }
                    }

                    if (GetCurrentValue(root) == Data) continue;

                    if (root.OpenSubKey(subKey) == null && Operation == RegistryValueOperation.Set) continue;
                    if (root.OpenSubKey(subKey) == null && Operation == RegistryValueOperation.Add) root.CreateSubKey(subKey);

                    if (Operation == RegistryValueOperation.Delete)
                    {
                        var key = root.OpenSubKey(subKey, true);
                        key?.DeleteValue(Value);
                        continue;
                    }

                    if (Type == RegistryValueType.REG_BINARY)
                    {
                        var data = StringToByteArray(Data.ToString());

                        Registry.SetValue(root.Name + "\\" + subKey, Value, data, (RegistryValueKind)Type);
                    }
                    else if (Type == RegistryValueType.REG_DWORD)
                    {
                        // DWORD values using the highest bit set fail without this, for example '2962489444'.
                        // See https://stackoverflow.com/questions/6608400/how-to-put-a-dword-in-the-registry-with-the-highest-bit-set;
                        var value = unchecked((int)Convert.ToUInt32(Data));
                        Registry.SetValue(root.Name + "\\" + subKey, Value, value, (RegistryValueKind)Type);
                    }
                    else if (Type == RegistryValueType.REG_QWORD)
                    {
                        Registry.SetValue(root.Name + "\\" + subKey, Value, Convert.ToUInt64(Data), (RegistryValueKind)Type);
                    }
                    else if (Type == RegistryValueType.REG_NONE)
                    {
                        byte[] none = new byte[0];

                        Registry.SetValue(root.Name + "\\" + subKey, Value, none, (RegistryValueKind)Type);
                    }
                    else if (Type == RegistryValueType.REG_MULTI_SZ)
                    {
                        string[] data;
                        if (Data.ToString() == "") data = new string[] { };
                        else data = Data.ToString().Split(new string[] { "\\0" }, StringSplitOptions.None);

                        Registry.SetValue(root.Name + "\\" + subKey, Value, data, (RegistryValueKind)Type);
                    }
                    else
                    {
                        Registry.SetValue(root.Name + "\\" + subKey, Value, Data, (RegistryValueKind)Type);
                    }
                }
                catch (Exception e)
                {
                    ErrorLogger.WriteToErrorLog(e.Message,
                        e.StackTrace, "RegistryValueAction Error", root?.Name + "\\" + subKey);
                }
            }
            return true;
        }
    }
}
