// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;

namespace System.DirectoryServices.ActiveDirectory
{
    /// <summary>
    /// Internal class that is used as a key in the hashtable
    /// of directory entries
    /// </summary>
    internal sealed class DistinguishedName
    {
        public DistinguishedName(string dn)
        {
            Components = Utils.GetDNComponents(dn);
        }

        public Component[] Components { get; }

        public bool Equals(DistinguishedName dn)
        {
            bool result = true;
            if ((dn == null) || (Components.GetLength(0) != dn.Components.GetLength(0)))
            {
                result = false;
            }
            else
            {
                for (int i = 0; i < Components.GetLength(0); i++)
                {
                    if ((Utils.Compare(Components[i].Name, dn.Components[i].Name) != 0)
                        || (Utils.Compare(Components[i].Value, dn.Components[i].Value) != 0))
                    {
                        result = false;
                        break;
                    }
                }
            }
            return result;
        }

        public override bool Equals([NotNullWhen(true)] object? obj)
        {
            if (obj is DistinguishedName other)
            {
                return Equals(other);
            }
            else
            {
                return false;
            }
        }

        public override int GetHashCode()
        {
            int hashCode = 0;
            for (int i = 0; i < Components.GetLength(0); i++)
            {
                hashCode = hashCode + Components[i].Name!.ToUpperInvariant().GetHashCode() + Components[i].Value!.ToUpperInvariant().GetHashCode();
            }
            return hashCode;
        }

        public override string ToString()
        {
            string dn = Components[0].Name + "=" + Components[0].Value;
            for (int i = 1; i < Components.GetLength(0); i++)
            {
                dn = dn + "," + Components[i].Name + "=" + Components[i].Value;
            }
            return dn;
        }
    }

    /// <summary>
    /// This class manages a list of directory entries
    /// for an object that needs to bind to several
    /// objects in AD. This maintains a cache of directory entries
    /// and creates a new directory entry (for a given dn) only if
    /// it doesn't already exist
    /// </summary>
    internal sealed class DirectoryEntryManager
    {
        private readonly Hashtable _directoryEntries = new Hashtable();
        private string? _bindingPrefix;
        private readonly DirectoryContext _context;
        private readonly NativeComInterfaces.IAdsPathname _pathCracker;

        internal DirectoryEntryManager(DirectoryContext context)
        {
            _context = context;
            _pathCracker = (NativeComInterfaces.IAdsPathname)new NativeComInterfaces.Pathname();
            _pathCracker.EscapedMode = NativeComInterfaces.ADS_ESCAPEDMODE_ON;
        }

        internal ICollection GetCachedDirectoryEntries() => _directoryEntries.Values;

        internal DirectoryEntry GetCachedDirectoryEntry(WellKnownDN dn)
        {
            return GetCachedDirectoryEntry(ExpandWellKnownDN(dn));
        }

        internal DirectoryEntry GetCachedDirectoryEntry(string distinguishedName)
        {
            // check if it's not RootDSE
            object dn = distinguishedName;

            if ((!string.Equals(distinguishedName, "rootdse", StringComparison.OrdinalIgnoreCase))
            && (!string.Equals(distinguishedName, "schema", StringComparison.OrdinalIgnoreCase)))
            {
                dn = new DistinguishedName(distinguishedName);
            }

            if (!_directoryEntries.ContainsKey(dn))
            {
                // directory entry does not exist
                // create a new one and cache it
                DirectoryEntry de = GetNewDirectoryEntry(distinguishedName);
                // add it to the cache
                _directoryEntries.Add(dn, de);
            }
            return (DirectoryEntry)_directoryEntries[dn]!;
        }

        internal void RemoveIfExists(string distinguishedName)
        {
            // check if it's not RootDSE
            object dn = distinguishedName;

            //
            // NOTE: Currently only comparing against "rootdse", but in the future if we are going to
            //           remove any other entries that are not in dn format (such as schema), we need to add the
            //           special casing here.
            //

            if (!string.Equals(distinguishedName, "rootdse", StringComparison.OrdinalIgnoreCase))
            {
                dn = new DistinguishedName(distinguishedName);
            }

            if (_directoryEntries.ContainsKey(dn))
            {
                DirectoryEntry? tmp = (DirectoryEntry?)_directoryEntries[dn];
                if (tmp != null)
                {
                    _directoryEntries.Remove(dn);
                    tmp.Dispose();
                }
            }
        }

        private DirectoryEntry GetNewDirectoryEntry(string dn)
        {
            _bindingPrefix ??= "LDAP://" + _context.GetServerName() + "/";

            _pathCracker.Set(dn, NativeComInterfaces.ADS_SETTYPE_DN);
            string escapedDN = _pathCracker.Retrieve(NativeComInterfaces.ADS_FORMAT_X500_DN);

            return Bind(_bindingPrefix + escapedDN, _context.UserName, _context.Password, _context.useServerBind());
        }

        internal string ExpandWellKnownDN(WellKnownDN dn)
        {
            string? distinguishedName = null;

            switch (dn)
            {
                case WellKnownDN.RootDSE:
                    {
                        distinguishedName = "RootDSE";
                        break;
                    }
                case WellKnownDN.RootDomainNamingContext:
                    {
                        DirectoryEntry rootDSE = GetCachedDirectoryEntry("RootDSE");

                        distinguishedName = (string)PropertyManager.GetPropertyValue(_context, rootDSE, PropertyManager.RootDomainNamingContext)!;
                        break;
                    }
                case WellKnownDN.DefaultNamingContext:
                    {
                        DirectoryEntry rootDSE = GetCachedDirectoryEntry("RootDSE");
                        distinguishedName = (string)PropertyManager.GetPropertyValue(_context, rootDSE, PropertyManager.DefaultNamingContext)!;
                        break;
                    }
                case WellKnownDN.SchemaNamingContext:
                    {
                        DirectoryEntry rootDSE = GetCachedDirectoryEntry("RootDSE");
                        distinguishedName = (string)PropertyManager.GetPropertyValue(_context, rootDSE, PropertyManager.SchemaNamingContext)!;
                        break;
                    }
                case WellKnownDN.ConfigurationNamingContext:
                    {
                        DirectoryEntry rootDSE = GetCachedDirectoryEntry("RootDSE");
                        distinguishedName = (string)PropertyManager.GetPropertyValue(_context, rootDSE, PropertyManager.ConfigurationNamingContext)!;
                        break;
                    }
                case WellKnownDN.PartitionsContainer:
                    {
                        distinguishedName = "CN=Partitions," + ExpandWellKnownDN(WellKnownDN.ConfigurationNamingContext);
                        break;
                    }
                case WellKnownDN.SitesContainer:
                    {
                        distinguishedName = "CN=Sites," + ExpandWellKnownDN(WellKnownDN.ConfigurationNamingContext);
                        break;
                    }
                case WellKnownDN.SystemContainer:
                    {
                        distinguishedName = "CN=System," + ExpandWellKnownDN(WellKnownDN.DefaultNamingContext);
                        break;
                    }
                case WellKnownDN.RidManager:
                    {
                        distinguishedName = "CN=RID Manager$," + ExpandWellKnownDN(WellKnownDN.SystemContainer);
                        break;
                    }
                case WellKnownDN.Infrastructure:
                    {
                        distinguishedName = "CN=Infrastructure," + ExpandWellKnownDN(WellKnownDN.DefaultNamingContext);
                        break;
                    }
                default:
                    // should not happen
                    throw new InvalidEnumArgumentException(nameof(dn), (int)dn, typeof(WellKnownDN));
            }
            return distinguishedName;
        }

        internal static DirectoryEntry GetDirectoryEntry(DirectoryContext context, WellKnownDN dn)
        {
            return GetDirectoryEntry(context, ExpandWellKnownDN(context, dn));
        }

        internal static DirectoryEntry GetDirectoryEntry(DirectoryContext context, string dn)
        {
            string tempBindingPrefix = "LDAP://" + context.GetServerName() + "/";

            NativeComInterfaces.IAdsPathname pathCracker = (NativeComInterfaces.IAdsPathname)new NativeComInterfaces.Pathname();
            pathCracker.EscapedMode = NativeComInterfaces.ADS_ESCAPEDMODE_ON;
            pathCracker.Set(dn, NativeComInterfaces.ADS_SETTYPE_DN);
            string escapedDN = pathCracker.Retrieve(NativeComInterfaces.ADS_FORMAT_X500_DN);

            return Bind(tempBindingPrefix + escapedDN, context.UserName, context.Password, context.useServerBind());
        }

        internal static DirectoryEntry GetDirectoryEntryInternal(DirectoryContext context, string path)
        {
            return Bind(path, context.UserName, context.Password, context.useServerBind());
        }

        internal static DirectoryEntry Bind(string ldapPath, string? username, string? password, bool useServerBind)
        {
            DirectoryEntry? de = null;
            AuthenticationTypes authType = Utils.DefaultAuthType;

            //
            // use ServerBind flag is the target is a server and the ServerBind option is supported
            //

            if (useServerBind)
            {
                authType |= AuthenticationTypes.ServerBind;
            }

            de = new DirectoryEntry(ldapPath, username, password, authType);
            return de;
        }

        internal static string ExpandWellKnownDN(DirectoryContext context, WellKnownDN dn)
        {
            string? distinguishedName = null;

            switch (dn)
            {
                case WellKnownDN.RootDSE:
                    {
                        distinguishedName = "RootDSE";
                        break;
                    }
                case WellKnownDN.RootDomainNamingContext:
                    {
                        DirectoryEntry rootDSE = GetDirectoryEntry(context, "RootDSE");

                        try
                        {
                            distinguishedName = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.RootDomainNamingContext)!;
                        }
                        finally
                        {
                            rootDSE.Dispose();
                        }
                        break;
                    }
                case WellKnownDN.DefaultNamingContext:
                    {
                        DirectoryEntry rootDSE = GetDirectoryEntry(context, "RootDSE");
                        try
                        {
                            distinguishedName = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.DefaultNamingContext)!;
                        }
                        finally
                        {
                            rootDSE.Dispose();
                        }
                        break;
                    }
                case WellKnownDN.SchemaNamingContext:
                    {
                        DirectoryEntry rootDSE = GetDirectoryEntry(context, "RootDSE");
                        try
                        {
                            distinguishedName = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.SchemaNamingContext)!;
                        }
                        finally
                        {
                            rootDSE.Dispose();
                        }
                        break;
                    }
                case WellKnownDN.ConfigurationNamingContext:
                    {
                        DirectoryEntry rootDSE = GetDirectoryEntry(context, "RootDSE");
                        try
                        {
                            distinguishedName = (string)PropertyManager.GetPropertyValue(context, rootDSE, PropertyManager.ConfigurationNamingContext)!;
                        }
                        finally
                        {
                            rootDSE.Dispose();
                        }
                        break;
                    }
                case WellKnownDN.PartitionsContainer:
                    {
                        distinguishedName = "CN=Partitions," + ExpandWellKnownDN(context, WellKnownDN.ConfigurationNamingContext);
                        break;
                    }
                case WellKnownDN.SitesContainer:
                    {
                        distinguishedName = "CN=Sites," + ExpandWellKnownDN(context, WellKnownDN.ConfigurationNamingContext);
                        break;
                    }
                case WellKnownDN.SystemContainer:
                    {
                        distinguishedName = "CN=System," + ExpandWellKnownDN(context, WellKnownDN.DefaultNamingContext);
                        break;
                    }
                case WellKnownDN.RidManager:
                    {
                        distinguishedName = "CN=RID Manager$," + ExpandWellKnownDN(context, WellKnownDN.SystemContainer);
                        break;
                    }
                case WellKnownDN.Infrastructure:
                    {
                        distinguishedName = "CN=Infrastructure," + ExpandWellKnownDN(context, WellKnownDN.DefaultNamingContext);
                        break;
                    }
                default:
                    // should not happen
                    throw new InvalidEnumArgumentException(nameof(dn), (int)dn, typeof(WellKnownDN));
            }
            return distinguishedName;
        }
    }
}
