using Concilium.Helpers;
using IrcDotNet;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;

namespace Concilium.Bot
{
    // Provides core functionality for an IRC bot that operates via multiple clients.
    public abstract class IrcBot : IDisposable
    {
        private const int clientQuitTimeout = 1000;

        // Dictionary of all chat command processors, keyed by name.
        private IDictionary<string, ChatCommandProcessor> chatCommandProcessors;

        // Dictionary of all command processors, keyed by name.
        private IDictionary<string, CommandProcessor> commandProcessors;

        // True if the read loop is currently active, false if ready to terminate.
        private bool isRunning;

        private bool isDisposed = false;

        public IrcBot()
        {
            this.isRunning = false;
            this.commandProcessors = new Dictionary<string, CommandProcessor>(StringComparer.InvariantCultureIgnoreCase);
            InitializeCommandProcessors();

            this.chatCommandProcessors = new Dictionary<string, ChatCommandProcessor>(StringComparer.InvariantCultureIgnoreCase);
            InitializeChatCommandProcessors();
        }

        ~IrcBot()
        {
            Dispose(false);
        }

        public virtual string QuitMessage
        {
            get { return "Big brother isn't watching"; }
        }

        protected IDictionary<string, ChatCommandProcessor> ChatCommandProcessors
        {
            get { return this.chatCommandProcessors; }
        }

        public StandardIrcClient Client
        {
            get;
            private set;
        }

        protected IDictionary<string, CommandProcessor> CommandProcessors
        {
            get { return this.commandProcessors; }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected void Dispose(bool disposing)
        {
            if (!this.isDisposed)
            {
                if (disposing)
                {
                    // Disconnect each client gracefully.
                    if (Client != null)
                    {
                        Client.Quit(clientQuitTimeout, this.QuitMessage);
                        Client.Dispose();
                    }
                }
            }
            this.isDisposed = true;
        }

        public void Run()
        {
            // Read commands from stdin until bot terminates.
            this.isRunning = true;
            while (this.isRunning)
            {
                Console.Write("> ");
                var line = Console.ReadLine();
                if (line == null)
                    break;
                if (line.Length == 0)
                    continue;

                var parts = line.Split(' ');
                var command = parts[0].ToLower();
                var parameters = parts.Skip(1).ToArray();
                ReadCommand(command, parameters);
            }
        }

        public void Stop()
        {
            this.isRunning = false;
        }

        protected abstract void InitializeCommandProcessors();

        private void ReadCommand(string command, IList<string> parameters)
        {
            CommandProcessor processor;
            if (this.commandProcessors.TryGetValue(command, out processor))
            {
                try
                {
                    processor(command, parameters);
                }
                catch (Exception ex)
                {
                    Log.Get().Error(string.Format("Error: {0}", ex.Message));
                }
            }
            else
            {
                Log.Get().Error(string.Format("Command '{0}' not recognized.", command));
            }
        }

        protected void Connect()
        {
            var registrationInfo = new IrcUserRegistrationInfo()
            {
                UserName = "Concilium",
                NickName = "Concilium",
                Password = "K3kst0ru5",
                RealName = "#sppro knowledge bot"
            };

            // Create new IRC client and connect to given server.
            Client = new StandardIrcClient();
            Client.FloodPreventer = new IrcStandardFloodPreventer(4, 2000);
            Client.Connected += IrcClient_Connected;
            Client.Disconnected += IrcClient_Disconnected;
            Client.Registered += IrcClient_Registered;
            Client.RawMessageReceived += IrcClient_RawMessageReceived;

            // Wait until connection has succeeded or timed out.
            using (var connectedEvent = new ManualResetEventSlim(false))
            {
                Client.Connected += (sender2, e2) => connectedEvent.Set();
                Client.Connect("irc.euirc.net", 6665, false, registrationInfo);
                if (!connectedEvent.Wait(10000))
                {
                    Client.Dispose();
                    Log.Get().Error("Connection timed out");
                    return;
                }
            }

            Log.Get().Info("Now connected");
        }

        public void Disconnect()
        {
            // Disconnect IRC client that is connected to given server.
            Client.Quit(clientQuitTimeout, this.QuitMessage);
            Client.Dispose();

            // Remove client from connection.
            Client = null;

            Log.Get().Info("Disconnected");
        }

        protected abstract void InitializeChatCommandProcessors();

        private bool ReadChatCommand(IrcClient client, IrcMessageEventArgs eventArgs)
        {
            // Check if given message represents chat command.
            var line = eventArgs.Text;
            if (line.Length > 1 && line.StartsWith(".") || line.StartsWith(">"))
            {
                // Process command.
                var parts = line.Split(' ');
                var command = parts.First();
                var parameters = parts.Skip(1).ToArray();
                ReadChatCommand(client, eventArgs.Source, eventArgs.Targets, command, parameters);
                return true;
            }
            return false;
        }

        private void ReadChatCommand(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string command, string[] parameters)
        {
            var defaultReplyTargets = GetDefaultReplyTargets(client, source, targets);

            ChatCommandProcessor processor;
            if (this.chatCommandProcessors.TryGetValue(command, out processor))
            {
                try
                {
                    processor(client, source, targets, command, parameters);
                }
                catch (InvalidCommandParametersException exInvalidCommandParameters)
                {
                    client.LocalUser.SendNotice(defaultReplyTargets, exInvalidCommandParameters.Message);
                }
                catch (Exception ex)
                {
                    if (source is IIrcMessageTarget)
                    {
                        client.LocalUser.SendNotice(defaultReplyTargets, string.Format("Error processing '{0}' command: {1}", command, ex.Message));
                    }
                }
            }
            else
            {
                if (source is IIrcMessageTarget)
                {
                    client.LocalUser.SendNotice(defaultReplyTargets, string.Format("Command '{0}' not recognized.", command));
                }
            }
        }

        protected virtual void OnRawMessageReceived(IrcClient client, IrcRawMessageEventArgs e) { }

        protected virtual void OnClientConnect(IrcClient client) { }

        protected virtual void OnClientDisconnect(IrcClient client) { }

        protected virtual void OnClientRegistered(IrcClient client) { }

        protected virtual void OnLocalUserJoinedChannel(IrcLocalUser localUser, IrcChannelEventArgs e) { }

        protected virtual void OnLocalUserLeftChannel(IrcLocalUser localUser, IrcChannelEventArgs e) { }

        protected virtual void OnLocalUserNoticeReceived(IrcLocalUser localUser, IrcMessageEventArgs e) { }

        protected virtual void OnLocalUserMessageReceived(IrcLocalUser localUser, IrcMessageEventArgs e) { }

        protected virtual void OnChannelUserJoined(IrcChannel channel, IrcChannelUserEventArgs e) { }

        protected virtual void OnChannelUserLeft(IrcChannel channel, IrcChannelUserEventArgs e) { }

        protected virtual void OnChannelNoticeReceived(IrcChannel channel, IrcMessageEventArgs e) { }

        protected virtual void OnChannelMessageReceived(IrcChannel channel, IrcMessageEventArgs e) { }

        #region IRC Client Event Handlers

        private void IrcClient_Connected(object sender, EventArgs e)
        {
            var client = (IrcClient)sender;

            OnClientConnect(client);
        }

        private void IrcClient_Disconnected(object sender, EventArgs e)
        {
            var client = (IrcClient)sender;

            OnClientDisconnect(client);
        }

        private void IrcClient_Registered(object sender, EventArgs e)
        {
            var client = (IrcClient)sender;

            client.LocalUser.NoticeReceived += IrcClient_LocalUser_NoticeReceived;
            client.LocalUser.MessageReceived += IrcClient_LocalUser_MessageReceived;
            client.LocalUser.JoinedChannel += IrcClient_LocalUser_JoinedChannel;
            client.LocalUser.LeftChannel += IrcClient_LocalUser_LeftChannel;

            OnClientRegistered(client);
        }

        private void IrcClient_LocalUser_NoticeReceived(object sender, IrcMessageEventArgs e)
        {
            var localUser = (IrcLocalUser)sender;

            OnLocalUserNoticeReceived(localUser, e);
        }

        private void IrcClient_LocalUser_MessageReceived(object sender, IrcMessageEventArgs e)
        {
            var localUser = (IrcLocalUser)sender;

            if (e.Source is IrcUser)
            {
                // Read message and process if it is chat command.
                if (ReadChatCommand(localUser.Client, e))
                    return;
            }

            OnLocalUserMessageReceived(localUser, e);
        }

        private void IrcClient_LocalUser_JoinedChannel(object sender, IrcChannelEventArgs e)
        {
            var localUser = (IrcLocalUser)sender;

            e.Channel.UserJoined += IrcClient_Channel_UserJoined;
            e.Channel.UserLeft += IrcClient_Channel_UserLeft;
            e.Channel.MessageReceived += IrcClient_Channel_MessageReceived;
            e.Channel.NoticeReceived += IrcClient_Channel_NoticeReceived;

            OnLocalUserJoinedChannel(localUser, e);
        }

        private void IrcClient_LocalUser_LeftChannel(object sender, IrcChannelEventArgs e)
        {
            var localUser = (IrcLocalUser)sender;

            e.Channel.UserJoined -= IrcClient_Channel_UserJoined;
            e.Channel.UserLeft -= IrcClient_Channel_UserLeft;
            e.Channel.MessageReceived -= IrcClient_Channel_MessageReceived;
            e.Channel.NoticeReceived -= IrcClient_Channel_NoticeReceived;

            OnLocalUserJoinedChannel(localUser, e);
        }

        private void IrcClient_RawMessageReceived(object sender, IrcRawMessageEventArgs e)
        {
            var client = (IrcClient)sender;

            OnRawMessageReceived(client, e);
        }

        private void IrcClient_Channel_UserLeft(object sender, IrcChannelUserEventArgs e)
        {
            var channel = (IrcChannel)sender;

            OnChannelUserJoined(channel, e);
        }

        private void IrcClient_Channel_UserJoined(object sender, IrcChannelUserEventArgs e)
        {
            var channel = (IrcChannel)sender;

            OnChannelUserLeft(channel, e);
        }

        private void IrcClient_Channel_NoticeReceived(object sender, IrcMessageEventArgs e)
        {
            var channel = (IrcChannel)sender;

            OnChannelNoticeReceived(channel, e);
        }

        private void IrcClient_Channel_MessageReceived(object sender, IrcMessageEventArgs e)
        {
            var channel = (IrcChannel)sender;

            if (e.Source is IrcUser)
            {
                // Read message and process if it is chat command.
                if (ReadChatCommand(channel.Client, e))
                    return;
            }

            OnChannelMessageReceived(channel, e);
        }

        #endregion

        public static IList<IIrcMessageTarget> GetDefaultReplyTargets(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets)
        {
            if (targets.Contains(client.LocalUser) && source is IIrcMessageTarget)
                return new[] { (IIrcMessageTarget)source };
            else
                return targets;
        }

        public static string GetDefaultReplyTarget(string _sSource, string _sTarget)
        {
            if (_sTarget == "Concilium")
                return _sSource;
            else
                return _sTarget;
        }

        protected delegate void ChatCommandProcessor(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string command, IList<string> parameters);

        protected delegate void CommandProcessor(string command, IList<string> parameters);

    }
}
