using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Capnp.Rpc
{
    /// <summary>
    /// A skeleton is a wrapper around a capability interface implementation which adapts it in the way it is
    /// expected by the <see cref="RpcEngine"/>.
    /// </summary>
    public abstract class Skeleton: IProvidedCapability
    {
        class SkeletonRelinquisher: IDisposable
        {
            readonly Skeleton _skeleton;

            public SkeletonRelinquisher(Skeleton skeleton)
            {
                _skeleton = skeleton;
            }

            public void Dispose()
            {
                _skeleton.Relinquish();
            }
        }

#if DEBUG_DISPOSE
        const int NoDisposeFlag = 0x4000000;
#endif

        static readonly ConditionalWeakTable<object, Skeleton> _implMap =
            new ConditionalWeakTable<object, Skeleton>();

        internal static Skeleton GetOrCreateSkeleton<T>(T impl, bool addRef)
            where T: class
        {
            if (impl is Skeleton skel)
                return skel;

            skel = _implMap.GetValue(impl, _ => CapabilityReflection.CreateSkeleton(_));

            if (addRef)
            {
                skel.Claim();
            }

            return skel;
        }

        /// <summary>
        /// Claims ownership on the given capability, preventing its automatic disposal.
        /// </summary>
        /// <typeparam name="T">Capability interface</typeparam>
        /// <param name="impl">Capability implementation</param>
        /// <returns>A disposable object. Calling Dispose() on the returned instance relinquishes ownership again.</returns>
        public static IDisposable Claim<T>(T impl) where T: class
        {
            return new SkeletonRelinquisher(GetOrCreateSkeleton(impl, true));
        }

#if DEBUG_DISPOSE
        /// <summary>
        /// This DEBUG-only diagnostic method states that the Skeleton corresponding to a given capability is not expected to
        /// be disposed until the next call to EndAssertNotDisposed().
        /// </summary>
        /// <typeparam name="T">Capability interface</typeparam>
        /// <param name="impl">Capability implementation</param>
        public static void BeginAssertNotDisposed<T>(T impl) where T : class
        {
            GetOrCreateSkeleton(impl, false).BeginAssertNotDisposed();
        }

        /// <summary>
        /// This DEBUG-only diagnostic method ends a non-disposal period started with BeginAssertNotDisposed.
        /// </summary>
        /// <typeparam name="T">Capability interface</typeparam>
        /// <param name="impl">Capability implementation</param>
        public static void EndAssertNotDisposed<T>(T impl) where T : class
        {
            GetOrCreateSkeleton(impl, false).EndAssertNotDisposed();
        }
#endif

        int _refCount = 0;

        /// <summary>
        /// Calls an interface method of this capability.
        /// </summary>
        /// <param name="interfaceId">ID of interface to call</param>
        /// <param name="methodId">ID of method to call</param>
        /// <param name="args">Method arguments ("params struct")</param>
        /// <param name="cancellationToken">Cancellation token, indicating when the call should cancelled.</param>
        /// <returns>A Task which will resolve to the call result</returns>
        public abstract Task<AnswerOrCounterquestion> Invoke(ulong interfaceId, ushort methodId, DeserializerState args, CancellationToken cancellationToken = default);

        internal void Claim()
        {
            Interlocked.Increment(ref _refCount);
        }

#if DEBUG_DISPOSE
        internal void BeginAssertNotDisposed()
        {
            if ((Interlocked.Add(ref _refCount, NoDisposeFlag) & NoDisposeFlag) == 0)
            {
                throw new InvalidOperationException("Flag already set. State is now broken.");
            }
        }
        internal void EndAssertNotDisposed()
        {
            if ((Interlocked.Add(ref _refCount, -NoDisposeFlag) & NoDisposeFlag) != 0)
            {
                throw new InvalidOperationException("Flag already cleared. State is now broken.");
            }
        }
#endif

        internal void Relinquish()
        {
            int count = Interlocked.Decrement(ref _refCount);

            if (0 == count)
            {
#if DEBUG_DISPOSE
                if ((count & NoDisposeFlag) != 0)
                    throw new InvalidOperationException("Unexpected Skeleton disposal");
#endif

                Dispose(true);
                GC.SuppressFinalize(this);
            }
        }

        internal void Relinquish(int count)
        {
            if (count < 0)
                throw new ArgumentOutOfRangeException(nameof(count));

            while (count-- > 0)
                Relinquish();
        }

        /// <summary>
        /// Dispose pattern implementation
        /// </summary>
        protected virtual void Dispose(bool disposing)
        {
        }

        /// <summary>
        /// Finalizer
        /// </summary>
        ~Skeleton()
        {
            Dispose(false);
        }

        internal virtual void Bind(object impl)
        {
            throw new NotSupportedException();
        }
    }

    /// <summary>
    /// Skeleton for a specific capability interface.
    /// </summary>
    /// <typeparam name="T">Capability interface</typeparam>
    public abstract class Skeleton<T> : Skeleton, IMonoSkeleton
    {
#if DebugEmbargos
        ILogger Logger { get; } = Logging.CreateLogger<Skeleton<T>>();
#endif

        Func<DeserializerState, CancellationToken, Task<AnswerOrCounterquestion>>[] _methods;
        CancellationTokenSource _disposed = new CancellationTokenSource();
        readonly object _reentrancyBlocker = new object();
        int _pendingCalls;

        /// <summary>
        /// Constructs an instance.
        /// </summary>
        public Skeleton()
        {
        }

        /// <summary>
        /// Populates this skeleton's method table. The method table maps method IDs (which are consecutively numbered from 0 
        /// onwards) to the underlying capability's method implementations.
        /// </summary>
        /// <param name="methods">The method table. Index is method ID.</param>
        protected void SetMethodTable(params Func<DeserializerState, CancellationToken, Task<AnswerOrCounterquestion>>[] methods)
        {
            _methods = methods;
        }

        /// <summary>
        /// Gets the underlying capability implementation.
        /// </summary>
        protected T Impl { get; private set; }

        /// <summary>
        /// Gets the ID of the implemented interface.
        /// </summary>
        public abstract ulong InterfaceId { get; }

        /// <summary>
        /// Calls an interface method of this capability.
        /// </summary>
        /// <param name="interfaceId">ID of interface to call</param>
        /// <param name="methodId">ID of method to call</param>
        /// <param name="args">Method arguments ("params struct")</param>
        /// <param name="cancellationToken">Cancellation token, indicating when the call should cancelled.</param>
        /// <returns>A Task which will resolve to the call result</returns>
        /// <exception cref="ObjectDisposedException">This Skeleton was disposed</exception>
        public override async Task<AnswerOrCounterquestion> Invoke(ulong interfaceId, ushort methodId, DeserializerState args, CancellationToken cancellationToken = default)
        {
            if (InterfaceId != InterfaceId)
                throw new NotImplementedException("Wrong interface id");

            if (methodId >= _methods.Length)
                throw new NotImplementedException("Wrong method id");

            lock (_reentrancyBlocker)
            {
                if (_disposed == null || _disposed.IsCancellationRequested)
                {
                    throw new ObjectDisposedException(nameof(Skeleton<T>));
                }

                ++_pendingCalls;
            }

            var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(_disposed.Token, cancellationToken);

            try
            {
                return await _methods[methodId](args, linkedSource.Token);
            }
            catch (System.Exception)
            {
                throw;
            }
            finally
            {
                lock (_reentrancyBlocker)
                {
                    --_pendingCalls;
                }

                linkedSource.Dispose();
                CheckCtsDisposal();
            }
        }

        void CheckCtsDisposal()
        {
            if (_pendingCalls == 0 && _disposed != null && _disposed.IsCancellationRequested)
            {
                _disposed.Dispose();
                _disposed = null;
            }
        }

        /// <summary>
        /// Dispose pattern implementation
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            lock (_reentrancyBlocker)
            {
                if (_disposed == null || _disposed.IsCancellationRequested)
                    return;

                _disposed.Cancel();

                CheckCtsDisposal();
            }

            if (Impl is IDisposable disposable)
            {
                disposable.Dispose();
            }
        }

        internal override void Bind(object impl)
        {
            if (Impl != null)
                throw new InvalidOperationException("Skeleton was already bound");

            Impl = (T)impl;
        }
    }
}