using System;
using System.Linq;
using System.Runtime.CompilerServices;

namespace Capnp.Rpc
{
    /// <summary>
    /// Provides functionality to construct Proxy and Skeleton instances from capability interfaces and objects implementing capability interfaces.
    /// A capability interface is any .NET interface which is annotated with <see cref="ProxyAttribute"/> and <see cref="SkeletonAttribute"/>.
    /// There are some intricacies to consider that you usually don't need to care about, since all that stuff will be generated.
    /// </summary>
    public static class CapabilityReflection
    {
        interface IBrokenFactory
        {
            System.Exception Exception { get; }
        }

        abstract class ProxyFactory
        {
            public abstract Proxy NewProxy();
        }

        class ProxyFactory<T>: ProxyFactory where T: Proxy, new()
        {
            public override Proxy NewProxy() => new T();
        }

        class BrokenProxyFactory: ProxyFactory, IBrokenFactory
        {
            readonly System.Exception _exception;

            public BrokenProxyFactory(System.Exception exception)
            {
                _exception = exception;
            }

            public System.Exception Exception => _exception;

            public override Proxy NewProxy()
            {
                throw _exception;
            }
        }

        abstract class SkeletonFactory
        {
            public abstract Skeleton NewSkeleton();
        }

        class SkeletonFactory<T>: SkeletonFactory where T: Skeleton, new()
        {
            public override Skeleton NewSkeleton() => new T();
        }

        class BrokenSkeletonFactory: SkeletonFactory, IBrokenFactory
        {
            System.Exception _exception;

            public BrokenSkeletonFactory(System.Exception exception)
            {
                _exception = exception;
            }

            public System.Exception Exception => _exception;

            public override Skeleton NewSkeleton()
            {
                throw _exception;
            }
        }

        class PolySkeletonFactory: SkeletonFactory
        {
            readonly SkeletonFactory[] _monoFactories;

            public PolySkeletonFactory(SkeletonFactory[] monoFactories)
            {
                _monoFactories = monoFactories;
            }

            public override Skeleton NewSkeleton()
            {
                var poly = new PolySkeleton();
                foreach (var fac in _monoFactories)
                {
                    poly.AddInterface(fac.NewSkeleton());
                }
                return poly;
            }
        }

        static ConditionalWeakTable<Type, ProxyFactory> _proxyMap =
            new ConditionalWeakTable<Type, ProxyFactory>();
        static ConditionalWeakTable<Type, SkeletonFactory> _skeletonMap =
            new ConditionalWeakTable<Type, SkeletonFactory>();

        static CapabilityReflection()
        {
            _proxyMap.Add(typeof(BareProxy), new ProxyFactory<BareProxy>());
            _proxyMap.Add(typeof(object), new ProxyFactory<BareProxy>());
        }

        static SkeletonFactory CreateMonoSkeletonFactory(SkeletonAttribute attr, Type[] genericArguments)
        {
            var skeletonClass = attr.SkeletonClass;
            if (genericArguments.Length > 0)
                skeletonClass = skeletonClass.MakeGenericType(genericArguments);

            return (SkeletonFactory)Activator.CreateInstance(
                typeof(SkeletonFactory<>)
                .MakeGenericType(skeletonClass))!;
        }

        static SkeletonFactory GetSkeletonFactory(Type type)
        {
            return _skeletonMap.GetValue(type, _ =>
            {
                try
                {
                    var attrs = (from iface in _.GetInterfaces()
                                 let generics = iface.GetGenericArguments()
                                 from attr in iface.GetCustomAttributes(typeof(SkeletonAttribute), false)
                                 select (Attr: (SkeletonAttribute)attr, Generics: generics)).ToArray();

                    if (attrs.Length == 0)
                        throw new InvalidCapabilityInterfaceException("No 'Skeleton' attribute defined, don't know how to create the skeleton");

                    if (attrs.Length == 1)
                    {
                        return CreateMonoSkeletonFactory(attrs[0].Attr, attrs[0].Generics);
                    }
                    else
                    {
                        var monoFactories = attrs.Select(a => CreateMonoSkeletonFactory(a.Attr, a.Generics)).ToArray();
                        return new PolySkeletonFactory(monoFactories);
                    }
                }
                catch (System.Exception exception)
                {
                    return new BrokenSkeletonFactory(exception);
                }
            });
        }

        /// <summary>
        /// Creates a Skeleton for a given interface implementation.
        /// </summary>
        /// <param name="obj">Interface implementation. Must implement at least one interface which is annotated with a <see cref="SkeletonAttribute"/>.</param>
        /// <returns>The Skeleton</returns>
        /// <exception cref="ArgumentNullException"><paramref name="obj"/> is null.</exception>
        /// <exception cref="InvalidCapabilityInterfaceException">No <see cref="SkeletonAttribute"/> found on implemented interface(s).</exception>
        /// <exception cref="InvalidOperationException">Mismatch between generic type arguments (if capability interface is generic).</exception>
        /// <exception cref="ArgumentException">Mismatch between generic type arguments (if capability interface is generic).</exception>
        /// <exception cref="System.Reflection.TargetInvocationException">Problem with instatiating the Skeleton (constructor threw exception).</exception>
        /// <exception cref="MemberAccessException">Caller does not have permission to invoke the Skeleton constructor.</exception>
        /// <exception cref="TypeLoadException">Problem with building the Skeleton type, or problem with loading some dependent class.</exception>
        public static Skeleton CreateSkeleton(object obj)
        {
            if (obj == null)
                throw new ArgumentNullException(nameof(obj));

            var factory = GetSkeletonFactory(obj.GetType());
            var skeleton = factory.NewSkeleton();
            skeleton.Bind(obj);
            return skeleton;
        }

        static ProxyFactory GetProxyFactory(Type type)
        {
            return _proxyMap.GetValue(type, _ =>
            {
                try
                {
                    var attrs = type
                        .GetCustomAttributes(typeof(ProxyAttribute), false)
                        .Cast<ProxyAttribute>()
                        .ToArray();

                    if (attrs.Length == 0)
                        throw new InvalidCapabilityInterfaceException("No 'Proxy' attribute defined, don't know how to create the proxy");

                    if (attrs.Length == 1)
                    {
                        Type proxyClass = attrs[0].ProxyClass;
                        Type[] genericArguments = type.GetGenericArguments();
                        if (genericArguments.Length > 0)
                            proxyClass = proxyClass.MakeGenericType(genericArguments);

                        return (ProxyFactory)Activator.CreateInstance(
                            typeof(ProxyFactory<>)
                            .MakeGenericType(proxyClass))!;
                    }
                    else
                    {
                        throw new InvalidCapabilityInterfaceException("Multiple 'Proxy' attributes defined, don't know which one to take");
                    }
                }
                catch (System.Exception exception)
                {
                    return new BrokenProxyFactory(exception);
                }
            });
        }


        /// <summary>
        /// Validates that a given type qualifies as cpapbility interface, throws <see cref="InvalidCapabilityInterfaceException"/> on failure.
        /// </summary>
        /// <param name="interfaceType">type to check</param>
        /// <exception cref="ArgumentNullException"><paramref name="interfaceType"/> is null.</exception>
        /// <exception cref="InvalidCapabilityInterfaceException">Given typ did not qualify as capability interface. 
        /// Message and probably InnterException give more details.</exception>
        public static void ValidateCapabilityInterface(Type interfaceType)
        {
            if (interfaceType == null)
            {
                throw new ArgumentNullException(nameof(interfaceType));
            }

            var proxyFactory = GetProxyFactory(interfaceType);

            if (proxyFactory is IBrokenFactory brokenFactory)
            {
                throw new InvalidCapabilityInterfaceException(
                    "Given type did not qualify as capability interface, see inner exception.",
                    brokenFactory.Exception);
            }
        }

        /// <summary>
        /// Checks whether a given type qualifies as cpapbility interface.
        /// </summary>
        /// <param name="interfaceType">type to check</param>
        /// <returns>true when <paramref name="interfaceType"/> is a capability interface</returns>
        /// <exception cref="ArgumentNullException"><paramref name="interfaceType"/> is null.</exception>
        public static bool IsValidCapabilityInterface(Type interfaceType)
        {
            if (interfaceType == null)
            {
                throw new ArgumentNullException(nameof(interfaceType));
            }

            var proxyFactory = GetProxyFactory(interfaceType);

            return !(proxyFactory is IBrokenFactory);
        }

        /// <summary>
        /// Constructs a Proxy for given capability interface and wraps it around given low-level capability.
        /// </summary>
        /// <typeparam name="TInterface">Capability interface. Must be annotated with <see cref="ProxyAttribute"/>.</typeparam>
        /// <param name="cap">low-level capability</param>
        /// <returns>The Proxy instance which implements <typeparamref name="TInterface"/>.</returns>
        /// <exception cref="ArgumentNullException"><paramref name="cap"/> is null.</exception>
        /// <exception cref="InvalidCapabilityInterfaceException"><typeparamref name="TInterface"/> did not qualify as capability interface.</exception>
        /// <exception cref="InvalidOperationException">Mismatch between generic type arguments (if capability interface is generic).</exception>
        /// <exception cref="ArgumentException">Mismatch between generic type arguments (if capability interface is generic).</exception>
        /// <exception cref="System.Reflection.TargetInvocationException">Problem with instatiating the Proxy (constructor threw exception).</exception>
        /// <exception cref="MemberAccessException">Caller does not have permission to invoke the Proxy constructor.</exception>
        /// <exception cref="TypeLoadException">Problem with building the Proxy type, or problem with loading some dependent class.</exception>
        public static Proxy CreateProxy<TInterface>(ConsumedCapability? cap)
        {
            var factory = GetProxyFactory(typeof(TInterface));
            var proxy = factory.NewProxy();
            proxy.Bind(cap);
            return proxy;
        }
    }
}