using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using NLog;
using NzbDrone.Common;
using NzbDrone.Common.EnsureThat;
using NzbDrone.Common.Messaging;
using NzbDrone.Common.TPL;

namespace NzbDrone.Core.Messaging.Events
{
    public class EventAggregator : IEventAggregator
    {
        private readonly Logger _logger;
        private readonly IServiceFactory _serviceFactory;
        private readonly TaskFactory _taskFactory;
        private readonly Dictionary<string, object> _eventSubscribers;

        private class EventSubscribers<TEvent>
            where TEvent : class, IEvent
        {
            public IHandle<TEvent>[] _syncHandlers;
            public IHandleAsync<TEvent>[] _asyncHandlers;
            public IHandleAsync<IEvent>[] _globalHandlers;

            public EventSubscribers(IServiceFactory serviceFactory)
            {
                _syncHandlers = serviceFactory.BuildAll<IHandle<TEvent>>()
                                              .OrderBy(GetEventHandleOrder)
                                              .ToArray();

                _globalHandlers = serviceFactory.BuildAll<IHandleAsync<IEvent>>()
                                              .ToArray();

                _asyncHandlers = serviceFactory.BuildAll<IHandleAsync<TEvent>>()
                                               .ToArray();
            }
        }

        public EventAggregator(Logger logger, IServiceFactory serviceFactory)
        {
            _logger = logger;
            _serviceFactory = serviceFactory;
            _taskFactory = new TaskFactory();
            _eventSubscribers = new Dictionary<string, object>();
        }

        public void PublishEvent<TEvent>(TEvent @event)
            where TEvent : class, IEvent
        {
            Ensure.That(@event, () => @event).IsNotNull();

            var eventName = GetEventName(@event.GetType());

            /*
                        int workerThreads;
                        int completionPortThreads;
                        ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);

                        int maxCompletionPortThreads;
                        int maxWorkerThreads;
                        ThreadPool.GetMaxThreads(out maxWorkerThreads, out maxCompletionPortThreads);


                        int minCompletionPortThreads;
                        int minWorkerThreads;
                        ThreadPool.GetMinThreads(out minWorkerThreads, out minCompletionPortThreads);

                        _logger.Warn("Thread pool state WT:{0} PT:{1}  MAXWT:{2} MAXPT:{3} MINWT:{4} MINPT:{5}", workerThreads, completionPortThreads, maxWorkerThreads, maxCompletionPortThreads, minWorkerThreads, minCompletionPortThreads);
            */

            _logger.Trace("Publishing {0}", eventName);

            EventSubscribers<TEvent> subscribers;
            lock (_eventSubscribers)
            {
                if (!_eventSubscribers.TryGetValue(eventName, out var target))
                {
                    _eventSubscribers[eventName] = target = new EventSubscribers<TEvent>(_serviceFactory);
                }

                subscribers = target as EventSubscribers<TEvent>;
            }

            //call synchronous handlers first.
            var handlers = subscribers._syncHandlers;
            foreach (var handler in handlers)
            {
                try
                {
                    _logger.Trace("{0} -> {1}", eventName, handler.GetType().Name);
                    handler.Handle(@event);
                    _logger.Trace("{0} <- {1}", eventName, handler.GetType().Name);
                }
                catch (Exception e)
                {
                    _logger.Error(e, "{0} failed while processing [{1}]", handler.GetType().Name, eventName);
                }
            }

            foreach (var handler in subscribers._globalHandlers)
            {
                var handlerLocal = handler;

                _taskFactory.StartNew(() =>
                {
                    handlerLocal.HandleAsync(@event);
                },
                        TaskCreationOptions.PreferFairness)
                .LogExceptions();
            }

            foreach (var handler in subscribers._asyncHandlers)
            {
                var handlerLocal = handler;

                _taskFactory.StartNew(() =>
                {
                    _logger.Trace("{0} ~> {1}", eventName, handlerLocal.GetType().Name);
                    handlerLocal.HandleAsync(@event);
                    _logger.Trace("{0} <~ {1}", eventName, handlerLocal.GetType().Name);
                },
                        TaskCreationOptions.PreferFairness)
                .LogExceptions();
            }
        }

        private static string GetEventName(Type eventType)
        {
            if (!eventType.IsGenericType)
            {
                return eventType.Name;
            }

            return string.Format("{0}<{1}>", eventType.Name.Remove(eventType.Name.IndexOf('`')), eventType.GetGenericArguments()[0].Name);
        }

        internal static int GetEventHandleOrder<TEvent>(IHandle<TEvent> eventHandler)
            where TEvent : class, IEvent
        {
            var method = eventHandler.GetType().GetMethod(nameof(eventHandler.Handle), new Type[] { typeof(TEvent) });

            if (method == null)
            {
                return (int)EventHandleOrder.Any;
            }

            var attribute = method.GetCustomAttributes(typeof(EventHandleOrderAttribute), true).FirstOrDefault() as EventHandleOrderAttribute;

            if (attribute == null)
            {
                return (int)EventHandleOrder.Any;
            }

            return (int)attribute.EventHandleOrder;
        }
    }
}
