using Discord.Commands;
using Nito.AsyncEx;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace Discord.Modules
{
    public class ModuleManager
	{
		public event EventHandler<ServerEventArgs> ServerEnabled = delegate { };
        public event EventHandler<ServerEventArgs> ServerDisabled = delegate { };
        public event EventHandler<ChannelEventArgs> ChannelEnabled = delegate { };
        public event EventHandler<ChannelEventArgs> ChannelDisabled = delegate { };

        public event EventHandler<ServerEventArgs> LeftServer = delegate { };
        public event EventHandler<ServerUpdatedEventArgs> ServerUpdated = delegate { };
        public event EventHandler<ServerEventArgs> ServerUnavailable = delegate { };
        public event EventHandler<ServerEventArgs> ServerAvailable = delegate { };
        
        public event EventHandler<ChannelEventArgs> ChannelCreated = delegate { };
        public event EventHandler<ChannelEventArgs> ChannelDestroyed = delegate { };
        public event EventHandler<ChannelUpdatedEventArgs> ChannelUpdated = delegate { };

        public event EventHandler<RoleEventArgs> RoleCreated = delegate { };
        public event EventHandler<RoleUpdatedEventArgs> RoleUpdated = delegate { };
        public event EventHandler<RoleEventArgs> RoleDeleted = delegate { };

        public event EventHandler<UserEventArgs> UserBanned = delegate { };
        public event EventHandler<UserEventArgs> UserJoined = delegate { };
        public event EventHandler<UserEventArgs> UserLeft = delegate { };
        public event EventHandler<UserUpdatedEventArgs> UserUpdated = delegate { };
        //public event EventHandler<UserEventArgs> UserPresenceUpdated = delegate { };
        //public event EventHandler<UserEventArgs> UserVoiceStateUpdated = delegate { };
        public event EventHandler<UserEventArgs> UserUnbanned = delegate { };
        public event EventHandler<ChannelUserEventArgs> UserIsTyping = delegate { };

        public event EventHandler<MessageEventArgs> MessageReceived = delegate { };
        public event EventHandler<MessageEventArgs> MessageSent = delegate { };
        public event EventHandler<MessageEventArgs> MessageDeleted = delegate { };
        public event EventHandler<MessageUpdatedEventArgs> MessageUpdated = delegate { };
        public event EventHandler<MessageEventArgs> MessageReadRemotely = delegate { };
        
		private readonly bool _useServerWhitelist, _useChannelWhitelist, _allowAll, _allowPrivate;
		private readonly ConcurrentDictionary<ulong, Server> _enabledServers;
		private readonly ConcurrentDictionary<ulong, Channel> _enabledChannels;
		private readonly ConcurrentDictionary<ulong, int> _indirectServers;
        private readonly AsyncLock _lock;

        public DiscordClient Client { get; }
        public IModule Instance { get; }
        public string Name { get; }
		public string Id { get; }
		public ModuleFilter FilterType { get; }

        public IEnumerable<Server> EnabledServers => _enabledServers.Select(x => x.Value);
		public IEnumerable<Channel> EnabledChannels => _enabledChannels.Select(x => x.Value);

		internal ModuleManager(DiscordClient client, IModule instance, string name, ModuleFilter filterType)
		{
            Client = client;
            Instance = instance;
            Name = name;
            FilterType = filterType;

            Id = name.ToLowerInvariant();
            _lock = new AsyncLock();

			_allowAll = filterType == ModuleFilter.None;
			_useServerWhitelist = filterType.HasFlag(ModuleFilter.ServerWhitelist);
			_useChannelWhitelist = filterType.HasFlag(ModuleFilter.ChannelWhitelist);
			_allowPrivate = filterType.HasFlag(ModuleFilter.AlwaysAllowPrivate);

            _enabledServers = new ConcurrentDictionary<ulong, Server>();
			_enabledChannels = new ConcurrentDictionary<ulong, Channel>();
			_indirectServers = new ConcurrentDictionary<ulong, int>();

			if (_allowAll || _useServerWhitelist) //Server-only events
			{
				client.ChannelCreated += (s, e) => { if (e.Server != null && HasServer(e.Server)) ChannelCreated(s, e); };
                //TODO: This *is* a channel update if the before/after voice channel is whitelisted
				//client.UserVoiceStateUpdated += (s, e) => { if (HasServer(e.Server)) UserVoiceStateUpdated(s, e); };
			}

			client.ChannelDestroyed += (s, e) => { if (HasChannel(e.Channel)) ChannelDestroyed(s, e); };
			client.ChannelUpdated += (s, e) => { if (HasChannel(e.After)) ChannelUpdated(s, e); };

			client.MessageReceived += (s, e) => { if (HasChannel(e.Channel)) MessageReceived(s, e); };
			client.MessageSent += (s, e) => { if (HasChannel(e.Channel)) MessageSent(s, e); };
			client.MessageDeleted += (s, e) => { if (HasChannel(e.Channel)) MessageDeleted(s, e); };
			client.MessageUpdated += (s, e) => { if (HasChannel(e.Channel)) MessageUpdated(s, e); };
			client.MessageAcknowledged += (s, e) => { if (HasChannel(e.Channel)) MessageReadRemotely(s, e); };

			client.RoleCreated += (s, e) => { if (HasIndirectServer(e.Server)) RoleCreated(s, e); };
			client.RoleUpdated += (s, e) => { if (HasIndirectServer(e.Server)) RoleUpdated(s, e); };
			client.RoleDeleted += (s, e) => { if (HasIndirectServer(e.Server)) RoleDeleted(s, e); };

			client.LeftServer += (s, e) => { if (HasIndirectServer(e.Server)) { DisableServer(e.Server); LeftServer(s, e); } };
			client.ServerUpdated += (s, e) => { if (HasIndirectServer(e.After)) ServerUpdated(s, e); };
			client.ServerUnavailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerUnavailable(s, e); };
			client.ServerAvailable += (s, e) => { if (HasIndirectServer(e.Server)) ServerAvailable(s, e); };

			client.UserJoined += (s, e) => { if (HasIndirectServer(e.Server)) UserJoined(s, e); };
			client.UserLeft += (s, e) => { if (HasIndirectServer(e.Server)) UserLeft(s, e); };
			client.UserUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserUpdated(s, e); };
			client.UserIsTyping += (s, e) => { if (HasChannel(e.Channel)) UserIsTyping(s, e); };
			//TODO: We aren't getting events from UserPresence if AllowPrivate is enabled, but the server we know that user through isn't on the whitelist
			//client.UserPresenceUpdated += (s, e) => { if (HasIndirectServer(e.Server)) UserPresenceUpdated(s, e); };
			client.UserBanned += (s, e) => { if (HasIndirectServer(e.Server)) UserBanned(s, e); };
			client.UserUnbanned += (s, e) => { if (HasIndirectServer(e.Server)) UserUnbanned(s, e); };
		}

		public void CreateCommands(string prefix, Action<CommandGroupBuilder> config)
		{
			var commandService = Client.Services.Get<CommandService>();
			commandService.CreateGroup(prefix, x =>
			{
				x.Category(Name);
				x.AddCheck(new ModuleChecker(this));
				config(x);
            });

		}
		public bool EnableServer(Server server)
		{
			if (server == null) throw new ArgumentNullException(nameof(server));
			if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");

            using (_lock.Lock())
                return EnableServerInternal(server);
		}
		public void EnableServers(IEnumerable<Server> servers)
		{
			if (servers == null) throw new ArgumentNullException(nameof(servers));
			if (servers.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(servers));
			if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");

            using (_lock.Lock())
            {
				foreach (var server in servers)
					EnableServerInternal(server);
            }
		}
		private bool EnableServerInternal(Server server)
		{
			if (_enabledServers.TryAdd(server.Id, server))
			{
				if (ServerEnabled != null)
					ServerEnabled(this, new ServerEventArgs(server));
				return true;
			}
			return false;
		}

		public bool DisableServer(Server server)
		{
			if (server == null) throw new ArgumentNullException(nameof(server));
            if (!_useServerWhitelist) return false;

            using (_lock.Lock())
            {
				if (_enabledServers.TryRemove(server.Id, out server))
				{
					if (ServerDisabled != null)
						ServerDisabled(this, new ServerEventArgs(server));
					return true;
				}
				return false;
			}
		}
		public void DisableAllServers()
		{
			if (!_useServerWhitelist) throw new InvalidOperationException("This module is not configured to use a server whitelist.");
            if (!_useServerWhitelist) return;

            using (_lock.Lock())
            {
				if (ServerDisabled != null)
				{
					foreach (var server in _enabledServers)
						ServerDisabled(this, new ServerEventArgs(server.Value));
				}
					
				_enabledServers.Clear();
			}
		}

		public bool EnableChannel(Channel channel)
		{
			if (channel == null) throw new ArgumentNullException(nameof(channel));
            if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist.");

            using (_lock.Lock())
                return EnableChannelInternal(channel);
		}
		public void EnableChannels(IEnumerable<Channel> channels)
		{
			if (channels == null) throw new ArgumentNullException(nameof(channels));
			if (channels.Contains(null)) throw new ArgumentException("Collection cannot contain null.", nameof(channels));
			if (!_useChannelWhitelist) throw new InvalidOperationException("This module is not configured to use a channel whitelist.");

            using (_lock.Lock())
            {
				foreach (var channel in channels)
					EnableChannelInternal(channel);
            }
		}
		private bool EnableChannelInternal(Channel channel)
		{
			if (_enabledChannels.TryAdd(channel.Id, channel))
			{
				var server = channel.Server;
				if (server != null)
				{
					int value = 0;
					_indirectServers.TryGetValue(server.Id, out value);
					value++;
					_indirectServers[server.Id] = value;
				}
				if (ChannelEnabled != null)
					ChannelEnabled(this, new ChannelEventArgs(channel));
				return true;
			}
			return false;
		}

		public bool DisableChannel(Channel channel)
		{
			if (channel == null) throw new ArgumentNullException(nameof(channel));
            if (!_useChannelWhitelist) return false;

            using (_lock.Lock())
            {
				Channel ignored;
				if (_enabledChannels.TryRemove(channel.Id, out ignored))
				{
					var server = channel.Server;
					if (server != null)
					{
						int value = 0;
						_indirectServers.TryGetValue(server.Id, out value);
						value--;
						if (value <= 0)
							_indirectServers.TryRemove(server.Id, out value);
						else
							_indirectServers[server.Id] = value;
					}
					if (ChannelDisabled != null)
						ChannelDisabled(this, new ChannelEventArgs(channel));
					return true;
				}
				return false;
			}
		}
		public void DisableAllChannels()
		{
            if (!_useChannelWhitelist) return;

            using (_lock.Lock())
            {
				if (ChannelDisabled != null)
				{
					foreach (var channel in _enabledChannels)
						ChannelDisabled(this, new ChannelEventArgs(channel.Value));
				}

				_enabledChannels.Clear();
				_indirectServers.Clear();
			}
		}

		public void DisableAll()
		{
			if (_useServerWhitelist)
				DisableAllServers();
			if (_useChannelWhitelist)
				DisableAllChannels();
		}

		internal bool HasServer(Server server) =>
			_allowAll ||
			_useServerWhitelist && _enabledServers.ContainsKey(server.Id);
		internal bool HasIndirectServer(Server server) =>
			_allowAll ||
			(_useServerWhitelist && _enabledServers.ContainsKey(server.Id)) || 
			(_useChannelWhitelist && _indirectServers.ContainsKey(server.Id));
		internal bool HasChannel(Channel channel)
		{
			if (_allowAll) return true;
            if (channel.IsPrivate) return _allowPrivate;

			if (_useChannelWhitelist && _enabledChannels.ContainsKey(channel.Id)) return true;
			if (_useServerWhitelist)
			{
				var server = channel.Server;
				if (server == null) return false;
				if (_enabledServers.ContainsKey(server.Id)) return true;
			}
			return false;
		}
	}
}