//  
//   Rebex Sample Code License
// 
//   Copyright 2026, REBEX CR s.r.o.
//   All rights reserved.
//   https://www.rebex.net/
// 
//   Permission to use, copy, modify, and/or distribute this software for any
//   purpose with or without fee is hereby granted.
// 
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//   OTHER DEALINGS IN THE SOFTWARE.
// 

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Security;
using System.Runtime.Versioning;
using Microsoft.Win32;
using Rebex.IO.FileSystem;
using Rebex.Security.Cryptography;

namespace Rebex.Samples
{
    [SupportedOSPlatform("windows")]
    public class RegistryFileSystemProvider : ReadWriteFileSystemProvider
    {
        private const int REGISTRY_PATH_IMMUTABLE_LEVEL = 1;
        private static readonly Dictionary<string, RegistryKey> _rootKeys;

        public RegistryFileSystemProvider(FileSystemProviderSettings settings = null)
            : base(settings)
        {
        }

        static RegistryFileSystemProvider()
        {
            _rootKeys = new Dictionary<string, RegistryKey>();
            _rootKeys[Registry.ClassesRoot.Name] = Registry.ClassesRoot;
            _rootKeys[Registry.CurrentConfig.Name] = Registry.CurrentConfig;
            _rootKeys[Registry.CurrentUser.Name] = Registry.CurrentUser;
            _rootKeys[Registry.LocalMachine.Name] = Registry.LocalMachine;
            _rootKeys[Registry.PerformanceData.Name] = Registry.PerformanceData;
            _rootKeys[Registry.Users.Name] = Registry.Users;
        }

        protected override NodeAttributes GetAttributes(NodeBase node)
        {
            return new NodeAttributes(FileAttributes.Normal);
        }

        protected override NodeTimeInfo GetTimeInfo(NodeBase node)
        {
            return new NodeTimeInfo();
        }

        protected override NodeBase GetChild(string name, DirectoryNode parent)
        {
            if (parent.IsRootDirectory)
            {
                string dirName = _rootKeys.Keys.SingleOrDefault(key => key.Equals(name, StringComparison.OrdinalIgnoreCase));
                return (dirName == null)
                    ? null
                    : new DirectoryNode(dirName, parent);
            }

            using (RegistryKey registryKey = PathToKey(parent.Path))
            {
                var childSubKeys = registryKey.GetSubKeyNames()
                    .Where(subKeyName => subKeyName.Equals(name, StringComparison.OrdinalIgnoreCase))
                    .Select(childName => new DirectoryNode(childName, parent));

                var child = childSubKeys.Concat<NodeBase>(registryKey.GetValueNames()
                    .Where(valueName => valueName.Equals(RegistryHelper.GetOriginalEmptyFileName(name), StringComparison.OrdinalIgnoreCase))
                    .Select(childName => new RegistryValueFile(childName, parent, null, RegistryValueKind.Unknown))).SingleOrDefault();

                return child;
            }
        }

        protected override IEnumerable<NodeBase> GetChildren(DirectoryNode parent, NodeType nodeType)
        {
            if (parent.IsRootDirectory)
            {
                return _rootKeys.Keys.Select(key => new DirectoryNode(key, parent));
            }

            using (RegistryKey registryKey = PathToKey(parent.Path))
            {
                IEnumerable<NodeBase> retChildren = Enumerable.Empty<NodeBase>();
                if (nodeType.HasFlag(NodeType.Directory))
                {
                    retChildren = retChildren.Union(registryKey.GetSubKeyNames()
                        .Select(childName => new DirectoryNode(childName, parent)));

                }

                if (nodeType.HasFlag(NodeType.File))
                {
                    retChildren = retChildren.Union(registryKey.GetValueNames()
                        .Select(childName => new RegistryValueFile(childName, parent, null, RegistryValueKind.Unknown)));
                }

                return retChildren.ToArray();
            }
        }

        protected override bool Exists(NodePath path, NodeType nodeType)
        {
            if (path.ParentPath.IsRootPath )
            {
                return isRootKey(path.FileName);
            }

            using (RegistryKey parentKey = PathToKey(path.ParentPath))
            {
                if (parentKey == null)
                {
                    return false;
                }

                if (nodeType == NodeType.Directory)
                {
                    return parentKey.GetSubKeyNames()
                        .Any(name => name.Equals(RegistryHelper.GetOriginalEmptyFileName(path.FileName), StringComparison.OrdinalIgnoreCase));
                }

                if (nodeType == NodeType.File)
                {
                    return parentKey.GetValueNames()
                        .Any(name => name.Equals(RegistryHelper.GetOriginalEmptyFileName(path.FileName), StringComparison.OrdinalIgnoreCase));
                }

                return false;
            }
        }

        private bool isRootKey(string keyName)
        {
            return _rootKeys.ContainsKey(keyName);
        }

        protected override long GetLength(NodeBase node)
        {
            using (NodeContent content = GetContent(node, NodeContentParameters.ReadAccess))
            {
                return content.GetStream().Length;
            }
        }

        protected override NodeContent GetContent(NodeBase node, NodeContentParameters contentParams)
        {
            RegistryValueKind regValueKind;
            object regValue;
            var regFile = node as RegistryValueFile;
            using (RegistryKey parentkey = PathToKey(node.Parent.Path))
            {
                regValueKind = parentkey.GetValueKind(regFile.OriginalName);
                regValue = parentkey.GetValue(regFile.OriginalName);
            }

            if (!IsRegistryValueAvailable(regValue, regValueKind))
            {
                return NodeContent.CreateDelayedWriteContent(new MemoryStream());
            }

            Stream stream = RegistryValueToStream(regValue, regValueKind);
            return NodeContent.CreateDelayedWriteContent(stream);
        }

        protected override DirectoryNode CreateDirectory(DirectoryNode parent, DirectoryNode child)
        {
            if (parent.IsRootDirectory)
            {
                throw new NotSupportedException();
            }

            using (RegistryKey parentKey = PathToKey(parent.Path))
            {
                parentKey.CreateSubKey(child.Name);
            }

            return child;
        }

        protected override FileNode CreateFile(DirectoryNode parent, FileNode child)
        {
            if (parent.IsRootDirectory)
            {
                throw new NotSupportedException();
            }

            using (RegistryKey parentKey = PathToKey(parent.Path))
            {
                parentKey.SetValue(child.Name, new byte[0], RegistryValueKind.Binary);
            }

            return child;
        }

        protected override FileNode MakeNewFileNode(string name, DirectoryNode parentNode)
        {
            return new RegistryValueFile(name, parentNode, null, RegistryValueKind.None);
        }

        protected override DirectoryNode MakeNewDirectoryNode(string name, DirectoryNode parentNode)
        {
            return new RegistryDirectory(name, parentNode);
        }

        protected override NodeBase Delete(NodeBase node)
        {
            if (node.IsRootDirectory)
            {
                throw new NotSupportedException();
            }

            using (RegistryKey parentKey = PathToKey(node.Parent.Path))
            {
                if (node.IsDirectory)
                {
                    parentKey.DeleteSubKey(node.Name);
                }
                else
                {
                    parentKey.DeleteValue(node.Name);
                }
            }

            return node;
        }

        protected override NodeBase Rename(NodeBase node, string newName)
        {
            throw new NotSupportedException();
        }

        protected override NodeBase SaveContent(NodeBase node, NodeContent content)
        {
            using (RegistryKey parentKey = PathToKey(node.Parent.Path))
            {
                RegistryValueKind valueKind = parentKey.GetValueKind(node.Name);

                object parsedValue = StreamToRegistryValue(content.GetStream(), valueKind);
                parentKey.SetValue(node.Name, parsedValue, valueKind);

                return new RegistryValueFile(node.Name, node.Parent, parsedValue, valueKind);
            }
        }

        protected override NodeBase SetAttributes(NodeBase node, NodeAttributes attributes)
        {
            return node;
        }

        protected override NodeBase SetTimeInfo(NodeBase node, NodeTimeInfo newTimeInfo)
        {
            return node;
        }

        private static Stream RegistryValueToStream(object regValue, RegistryValueKind valueKind)
        {
            byte[] valueBytes;
            switch (valueKind)
            {
                case RegistryValueKind.ExpandString:
                case RegistryValueKind.MultiString:
                case RegistryValueKind.String:
                    valueBytes = Encoding.Unicode.GetBytes((string)regValue);
                    break;

                case RegistryValueKind.Binary:
                    valueBytes = regValue as byte[];
                    break;

                case RegistryValueKind.DWord:
                    valueBytes = BitConverter.GetBytes((int)regValue);
                    break;

                case RegistryValueKind.QWord:
                    valueBytes = BitConverter.GetBytes((long)regValue);
                    break;

                default:
                    throw new InvalidOperationException("Unsupported registry value kind.");
            }

            var valueStream = new MemoryStream();
            valueStream.Write(valueBytes, 0, valueBytes.Length);
            valueStream.Position = 0;
            return valueStream;
        }

        private static object StreamToRegistryValue(Stream regStream, RegistryValueKind valueKind)
        {
            var valueStream = new MemoryStream();
            regStream.CopyTo(valueStream);

            byte[] valueBytes = valueStream.GetBuffer();
            int valueLength = (int)valueStream.Length;

            switch (valueKind)
            {
                case RegistryValueKind.ExpandString:
                case RegistryValueKind.MultiString:
                case RegistryValueKind.String:
                    return Encoding.Unicode.GetString(valueBytes, 0, valueLength);

                case RegistryValueKind.Binary:
                    return valueStream.ToArray();

                case RegistryValueKind.DWord:
                    if (valueLength < 4)
                    {
                        throw new InvalidOperationException("Insufficient data.");
                    }
                    return BitConverter.ToInt32(valueBytes, 0);

                case RegistryValueKind.QWord:
                    if (valueLength < 8)
                    {
                        throw new InvalidOperationException("Insufficient data.");
                    }
                    return BitConverter.ToInt64(valueBytes, 0);

                default:
                    throw new InvalidOperationException("Unsupported registry value kind.");
            }
        }

        private static bool IsRegistryValueAvailable(object value, RegistryValueKind valueKind)
        {
            return (value != null) && (valueKind != RegistryValueKind.Unknown) && (valueKind != RegistryValueKind.None);
        }

        private RegistryKey PathToKey(NodePath path)
        {
            const int ROOT_KEY_INDEX = 0;
            NodePath currentPath = path;
            string registryRootKeyName = currentPath[ROOT_KEY_INDEX];
            RegistryKey registryRootKey = _rootKeys[registryRootKeyName];

            if (currentPath.PathParts.Count() == REGISTRY_PATH_IMMUTABLE_LEVEL)
            {
                return registryRootKey;
            }

            string registryKeyPath = currentPath
                .Skip(REGISTRY_PATH_IMMUTABLE_LEVEL)
                .Aggregate(new StringBuilder(),
                    (builder, pathPart) => builder.AppendFormat(@"{0}\", pathPart),
                    builder => builder.ToString());

            return registryRootKey.OpenSubKey(registryKeyPath, writable: true);
        }
    }

    public static class RegistryHelper
    {
        private const string EMPTY_NAME = "___default empty name___";
        private const char REPLACE_CHAR = '_';
        private static readonly char[] INVALID_FILE_NAME_CHARS = NodePath.GetInvalidFileNameChars();

        public static string GetSafeFileName(string originalName)
        {
            if (String.IsNullOrEmpty(originalName))
            {
                return EMPTY_NAME;
            }

            char[] invalidCharsInName = INVALID_FILE_NAME_CHARS.Where(originalName.Contains).ToArray();

            if (!invalidCharsInName.Any())
            {
                return originalName;
            }

            return invalidCharsInName.Aggregate(originalName,
                (currentName, invalidChar) => currentName.Replace(invalidChar, REPLACE_CHAR));
        }

        public static string GetOriginalEmptyFileName(string emptyName)
        {
            return Equals(emptyName, EMPTY_NAME)
                ? String.Empty
                : emptyName;
        }
    }

    public class RegistryDirectory : DirectoryNode
    {
        public RegistryDirectory(string nodeName, DirectoryNode parent)
            : base(parent != null ? RegistryHelper.GetSafeFileName(nodeName) : nodeName, parent)
        {
            OriginalName = nodeName;
        }

        public string OriginalName { get; private set; }
    }

    public class RegistryValueFile : FileNode
    {
        public RegistryValueFile(string nodeName, DirectoryNode parent, object rawContent, RegistryValueKind contentKind)
            : base(RegistryHelper.GetSafeFileName(nodeName), parent)
        {
            RawContent = rawContent;
            ContentKind = contentKind;
            OriginalName = nodeName;
        }

        public object RawContent { get; private set; }

        public RegistryValueKind ContentKind { get; private set; }

        public string OriginalName { get; private set; }
    }

}
