using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

#if UNITY_EDITOR
using System.ComponentModel;
#endif

namespace UnityEngine.InputSystem.Utilities
{
    /// <summary>
    /// A table mapping names to types in a case-insensitive mapping.
    /// </summary>
    internal struct TypeTable
    {
        public Dictionary<InternedString, Type> table;

        public IEnumerable<string> names => table.Keys.Select(x => x.ToString());
        public IEnumerable<InternedString> internedNames => table.Keys;

        // In the editor, we want to keep track of when the same type gets registered multiple times
        // with different names so that we can keep the aliases out of the UI.
        #if UNITY_EDITOR
        public HashSet<InternedString> aliases;
        #endif

        // Strong coupling to Input Manager which is always the owner
        private InputManager m_Manager;

        public void Initialize(InputManager manager)
        {
            table = new Dictionary<InternedString, Type>();
            #if UNITY_EDITOR
            aliases = new HashSet<InternedString>();
            #endif
            m_Manager = manager;
        }

        public InternedString FindNameForType(Type type)
        {
            if (type == null)
                throw new ArgumentNullException(nameof(type));
            foreach (var pair in table)
                if (pair.Value == type)
                    return pair.Key;
            return new InternedString();
        }

        public void AddTypeRegistration(string name, Type type)
        {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentException("Name cannot be null or empty", nameof(name));
            if (type == null)
                throw new ArgumentNullException(nameof(type));

            var internedName = new InternedString(name);

            #if UNITY_EDITOR
            // First check if the name has already been added to the table
            // This allows safely calling this function multiple times with same name
            // and prevents adding an alias to itself
            Type originalType;
            if (table.TryGetValue(internedName, out originalType))
            {
                // We don't warn about overriding any existing name/type mapping as that was previously supported
                table.Remove(internedName);
            }
            else
            {
                if (table.ContainsValue(type))
                    aliases.Add(internedName);
            }
            #endif

            table[internedName] = type;
        }

        public Type LookupTypeRegistration(string name)
        {
            if (string.IsNullOrEmpty(name))
                return null;

            if (table == null)
                throw new InvalidOperationException("Input System not yet initialized");

            return TryLookupTypeRegistration(new InternedString(name));
        }

        private Type TryLookupTypeRegistration(InternedString internedName)
        {
            if (!table.TryGetValue(internedName, out var type))
            {
                // Failed to look-up type, either type do not exist or it is a custom type that has not been registered.
                // Check whether we have attempted to load custom types and otherwise lazily load  types only when
                // relevant and reattempt looking up type by name. (ISXB-1766)
                if (m_Manager != null)
                {
                    if (m_Manager.RegisterCustomTypes())
                        table.TryGetValue(internedName, out type);
                }
            }

            return type;
        }

        #if UNITY_EDITOR
        public bool ShouldHideInUI(string name)
        {
            // Always hide aliases.
            if (aliases.Contains(new InternedString(name)))
                return true;

            // Hide entries that have [DesignTimeVisible(false)] on the type.
            var type = LookupTypeRegistration(name);
            var attribute = type?.GetCustomAttribute<DesignTimeVisibleAttribute>();
            return !(attribute?.Visible ?? true);
        }

        #endif
    }
}