using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Text.RegularExpressions;
using System.Net;
using System.Web;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json;
using System.IO;
using System.Xml;
using System.ServiceModel.Syndication;
using System.Net.Sockets;
using System.Collections;
using Concilium.Helpers;
using System.Net.NetworkInformation;
using System.Diagnostics;
using System.Drawing;
using IrcDotNet;
using System.Reflection;

namespace Concilium.Bot
{
    public class RssItem
    {
        public string Title;
        public string Link;
    }

    public class RssFeed
    {
        public string Url;
        public List<RssItem> LastItems;
    }

    public class UserInfo
    {
        public DateTime? LastWritten;
        public DateTime? LastJoined;
        public DateTime? LastLeft;

        public List<RssFeed> SubscribedFeeds;
    }

    public class BotData
    {
        public Dictionary<string, UserInfo> Info;
        public Dictionary<string, List<string>> ChannelUsers;
        public List<string> LogBlackList;
        public Mafia Mafia;
        public Resistance Resistance;
        public PredicateLogic PredicateLogic;
    }

    public class Concilium : IrcBot
    {
        private Timer userMonitorTimer;
        private Timer saveTimer;
        private Timer rssTimer;
        public static BotData Data;
        private List<IBotFeature> botFeatures = new List<IBotFeature>();

        protected override void InitializeChatCommandProcessors()
        {
            foreach (var type in Assembly.GetExecutingAssembly().GetTypes())
            {
                var attributes = type.GetCustomAttributes(typeof(BotFeatureAttribute), true).Cast<BotFeatureAttribute>();
                if (attributes.Count() == 0)
                    continue;

                var botFeature = Activator.CreateInstance(type) as IBotFeature;
                botFeatures.Add(botFeature);
                foreach (var attribute in attributes)
                {
                    var name = attribute.Name;
                    ChatCommandProcessors.Add(name, (client, source, targets, message, parameters) =>
                    {
                        string sIp = null;
                        if (source is IrcUser)
                        {
                            sIp = GetIP((source as IrcUser).HostName);
                        }
                        botFeature.Execute(name, source.Name, targets.First().Name, message, parameters, sIp);
                    });
                }
            }
            ChatCommandProcessors.Add(".random", ProcessChatRandom);
            ChatCommandProcessors.Add(".log", ProcessLog);
            ChatCommandProcessors.Add(".help", ProcessHelp);
            ChatCommandProcessors.Add(".admin", ProcessAdmin);
            ChatCommandProcessors.Add(".blacklist", ProcessBlackList);
            ChatCommandProcessors.Add(">", ProcessPredicateLogic);
        }

        protected override void InitializeCommandProcessors()
        {
        }

        public Concilium()
        {
            Load();
            Connect();
        }

        protected override void OnClientRegistered(IrcClient client)
        {
            base.OnClientRegistered(client);

            Client.Channels.Join("#makegames");
            Client.Channels.Join("#sppro");

            foreach (var botFeature in botFeatures)
            {
                botFeature.Client = Client;
                botFeature.Load();
            }
        }

        protected override void OnChannelMessageReceived(IrcChannel channel, IrcMessageEventArgs e)
        {
            UpdateLastWrite(e.Source.Name, e.Text, e.Targets.Select(t => t.Name));
        }

        protected override void OnLocalUserMessageReceived(IrcLocalUser localUser, IrcMessageEventArgs e)
        {
            UpdateLastWrite(e.Source.Name, e.Text, e.Targets.Select(t => t.Name));
        }

        protected void UpdateLastWrite(string _sNickName, string _sMessage, IEnumerable<string> _targets)
        {
            if (!Concilium.Data.Info.ContainsKey(_sNickName))
                Concilium.Data.Info[_sNickName] = new UserInfo();

            Concilium.Data.Info[_sNickName].LastWritten = DateTime.Now;

            var target = _targets.FirstOrDefault();
            string channel;
            if (string.IsNullOrEmpty(target))
                channel = "#makegames";//return;
            else
                channel = target;

            if (channel[0] == '#')
                channel = channel.Substring(1);

            var now = DateTime.Now.Year.ToString() + '-' + DateTime.Now.Month.ToString() + "-" + DateTime.Now.Day.ToString();
            var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Concilium", channel, now + ".log");
            Directory.CreateDirectory(Path.GetDirectoryName(path));
            using (var stream = new FileStream(path, FileMode.Append))
            {
                var message = DateTime.Now.Hour + ":" + DateTime.Now.Minute + ":" + DateTime.Now.Second + " " + _sNickName + " " + _sMessage + "\r\n";
                var bytes = Encoding.UTF8.GetBytes(message);
                stream.Write(bytes, 0, bytes.Length);
            }
        }

        protected override void OnLocalUserJoinedChannel(IrcLocalUser localUser, IrcChannelEventArgs e)
        {
            if (!Concilium.Data.ChannelUsers.ContainsKey(e.Channel.Name))
            {
                Concilium.Data.ChannelUsers[e.Channel.Name] = new List<string>();
            }
        }

        protected override void OnLocalUserLeftChannel(IrcLocalUser localUser, IrcChannelEventArgs e)
        {
            if (Concilium.Data.ChannelUsers.ContainsKey(e.Channel.Name))
            {
                Concilium.Data.ChannelUsers.Remove(e.Channel.Name);
            }
        }

        public void Load()
        {
            if (Concilium.Data != null)
                return;

            saveTimer = new Timer((object botImpl) =>
            {
                this.Save();
            }, this, 0, 60000);

            var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Concilium", "data.json");
            if (File.Exists(path))
            {
                Log.Get().Info(string.Format("Loading Concilium.Data from: {0}", path));
                var dataJson = File.ReadAllText(path);
                Concilium.Data = JsonConvert.DeserializeObject<BotData>(dataJson);
            }

            if (Concilium.Data == null)
                Concilium.Data = new BotData();
            if (Concilium.Data.Info == null)
                Concilium.Data.Info = new Dictionary<string, UserInfo>();
            if (Concilium.Data.ChannelUsers == null)
                Concilium.Data.ChannelUsers = new Dictionary<string, List<string>>();
            if (Concilium.Data.LogBlackList == null)
                Concilium.Data.LogBlackList = new List<string>();
            if (Concilium.Data.PredicateLogic == null)
                Concilium.Data.PredicateLogic = new PredicateLogic();
            if (Concilium.Data.Mafia == null)
                Concilium.Data.Mafia = new Mafia();
            if (Concilium.Data.Resistance == null)
                Concilium.Data.Resistance = new Resistance();

            foreach (var info in Concilium.Data.Info)
            {
                if (info.Value.SubscribedFeeds == null)
                    info.Value.SubscribedFeeds = new List<RssFeed>();

                foreach (var feed in info.Value.SubscribedFeeds)
                {
                    if (feed.LastItems == null)
                        feed.LastItems = new List<RssItem>();
                }
            }
        }

        public void Save()
        {
            if (Concilium.Data == null)
                return;

            var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Concilium", "data.json");
            Log.Get().Info(string.Format("Saving data to: {0}", path));
            var dataJson = JsonConvert.SerializeObject(Concilium.Data, Newtonsoft.Json.Formatting.Indented);
            while (true)
            {
                try
                {
                    File.WriteAllText(path, dataJson);
                    break;
                }
                catch (Exception)
                {
                    Thread.Sleep(1000);
                }
            }
        }

        private List<string> GetParameters(string message)
        {
            return CmdLineToArgvW.SplitArgs(message).ToList();
        }

        public static Color ColorFromHSV(double hue, double saturation, double value)
        {
            int hi = Convert.ToInt32(Math.Floor(hue / 60)) % 6;
            double f = hue / 60 - Math.Floor(hue / 60);

            value = value * 255;
            int v = Convert.ToInt32(value);
            int p = Convert.ToInt32(value * (1 - saturation));
            int q = Convert.ToInt32(value * (1 - f * saturation));
            int t = Convert.ToInt32(value * (1 - (1 - f) * saturation));

            if (hi == 0)
                return Color.FromArgb(255, v, t, p);
            else if (hi == 1)
                return Color.FromArgb(255, q, v, p);
            else if (hi == 2)
                return Color.FromArgb(255, p, v, t);
            else if (hi == 3)
                return Color.FromArgb(255, p, q, v);
            else if (hi == 4)
                return Color.FromArgb(255, t, p, v);
            else
                return Color.FromArgb(255, v, p, q);
        }

        private void ProcessHelp(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "I understand following commands: " + string.Join(" ", ChatCommandProcessors.Keys));
        }

        private void ProcessBlackList(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            if (!IsOp(source))
                return;

            if (parameters.Count < 1)
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Usage: .blacklist {show|add nickname|del nickname}.");
                return;
            }

            if (parameters[0] == "show")
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Blacklisted members: " + string.Join(", ", Concilium.Data.LogBlackList));
            }
            else if (parameters[0] == "add")
            {
                if (parameters.Count > 1 && !string.IsNullOrEmpty(parameters[1]))
                {
                    Concilium.Data.LogBlackList.Add(parameters[1]);
                    Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Added " + parameters[1] + " to the black list.");
                }
                else
                {
                    Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Can't add to black list. Nickname was not specified.");
                }
            }
            else if (parameters[0] == "del")
            {
                if (parameters.Count > 1 && !string.IsNullOrEmpty(parameters[1]))
                {
                    if (Concilium.Data.LogBlackList.Remove(parameters[1]))
                    {
                        Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Removed " + parameters[1] + " from the black list.");
                    }
                    else
                    {
                        Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), parameters[1] + " was not in the black list.");
                    }
                }
                else
                {
                    Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Can't add to black list. Nickname was not specified.");
                }
            }
            else
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Usage: .blacklist {show|add nickname|del nickname}.");
                return;
            }
        }

        private void ProcessPredicateLogic(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            var input = string.Join(" ", parameters);
            var output = Concilium.Data.PredicateLogic.Parse(input);
            if (!string.IsNullOrEmpty(output))
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), output);
        }

        private void ProcessLog(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            if (parameters.Count < 2)
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Usage: .log number unit (highlight). Unit is either s(econd), m(inute), h(our), d(ay) or w(eek).");
                return;
            }

            int number = 0;
            if (!int.TryParse(parameters[0], out number))
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "First parameter must be an integer.");
                return;
            }

            string unit = parameters[1].ToLower();

            TimeSpan logDuration = new TimeSpan();
            if (unit == "s")
                logDuration = TimeSpan.FromSeconds(number);
            else if (unit == "m")
                logDuration = TimeSpan.FromMinutes(number);
            else if (unit == "h")
                logDuration = TimeSpan.FromHours(number);
            else if (unit == "d")
                logDuration = TimeSpan.FromDays(number);
            else if (unit == "w")
                logDuration = TimeSpan.FromDays(7 * number);
            else
            {
                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Second parameter must be either s(econd), m(inute), h(our), d(ay) or w(eek).");
                return;
            }

            if (logDuration > TimeSpan.FromHours(2) && !IsOp(source))
                logDuration = TimeSpan.FromHours(2);

            DateTime startDate = DateTime.Now - logDuration;
            DateTime endDate = new DateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day + 1);
            endDate.AddMilliseconds(-1);

            var target = targets.FirstOrDefault();
            string channel;
            if (target == null || string.IsNullOrEmpty(target.Name))
                channel = "#makegames";//return;
            else
                channel = target.Name;

            if (channel[0] == '#')
                channel = channel.Substring(1);

            var userColors = new Dictionary<string, System.Drawing.Color>();
            var log = new List<dynamic>();
            for (DateTime date = startDate; date <= endDate; date = date.AddDays(1))
            {
                var now = date.Year.ToString() + '-' + date.Month.ToString() + "-" + date.Day.ToString();
                var path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "Concilium", channel, now + ".log");
                if (File.Exists(path))
                {
                    using (var sr = new StreamReader(path))
                    {
                        log.Add(date.ToLongDateString());

                        string sMessage = sr.ReadLine();
                        while (sMessage != null)
                        {
                            var tokens = sMessage.ReverseFormat("{0}:{1}:{2} {3} {4}");
                            var logDay = new DateTime(date.Year, date.Month, date.Day, int.Parse(tokens[0]), int.Parse(tokens[1]), int.Parse(tokens[2]));
                            if (logDay > startDate)
                            {
                                var nickName = tokens[3];
                                var messageContent = tokens[4];

                                log.Add(new
                                {
                                    time = logDay.ToLongTimeString(),
                                    nick = nickName,
                                    message = messageContent
                                });

                                userColors[nickName] = new Color();
                            }

                            sMessage = sr.ReadLine();
                        }
                    }
                }
            }

            string result = "<html><head><style type=\"text/css\">";
            result += "body, document, table { font-family: Verdana, sans-serif; background: #222; color: white; margin: 0; padding: 0; width: 100%; } ";
            result += "p { margin: 0px } ";
            result += "a { color: #80A0FF } ";
            result += "a:visited { color: #A080FF } ";
            result += ".date { font-size: 18px; margin: 5px; } ";
            result += ".time { font-size: 10px; margin: 2px; width: 80px; } ";
            result += ".nick { font-weight: bold; margin: 2px; text-align: right; width: 1px; } ";
            result += ".highlight { background: #FFE0A0; color: #102040; } ";
            result += "</style></head><body><table>";

            double angle = 0.0;
            var users = userColors.Keys.ToList();
            foreach (var user in users)
            {
                userColors[user] = ColorFromHSV(angle, 0.75, 0.75);
                angle += 360.0 / users.Count;
            }

            foreach (var logEntry in log)
            {
                if (logEntry is string)
                {
                    result += "<tr><td colspan=\"3\" class=\"date\">" + (logEntry as string) + "</td></tr>";
                }
                else
                {
                    var colorString = ColorTranslator.ToHtml(userColors[logEntry.nick]);
                    result += "<tr>";
                    result += "<td class=\"time\">" + logEntry.time + "</td>";
                    result += "<td class=\"nick\" style=\"color:" + colorString + "\">&lt;" + logEntry.nick + "&gt;</td>";

                    var sMessage = Regex.Replace(logEntry.message, "http\\://[a-zA-Z0-9\\-\\.]+(/\\S*)?", "<a href=\"$&\">$&</a>");
                    sMessage = Regex.Replace(sMessage, "https\\://[a-zA-Z0-9\\-\\.]+(/\\S*)?", "<a href=\"$&\">$&</a>");
                    result += "<td class=\"msg\">" + HttpUtility.HtmlEncode(sMessage) + "</td>";
                    result += "</tr>";
                }
            }

            result += "</html></body>";
            if (parameters.Count > 2)
                result = Regex.Replace(result, parameters[2], "<span class=\"highlight\">$&</span>");

            using (var wc = new WebClient())
            {
                var reqparm = new System.Collections.Specialized.NameValueCollection();
                reqparm.Add("file", result);
                byte[] responsebytes = wc.UploadValues("http://xTr1m.com/conciliumlog.php", "POST", reqparm);
                string responsebody = Encoding.UTF8.GetString(responsebytes);

                Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), "Log updated: http://www.xTr1m.com/temp/concilium.html");
            }
        }

        private bool IsOp(IIrcMessageSource source)
        {
            if (source.Name == "xTr1m")
                return true;

            return source.Name.StartsWith("!") || source.Name.StartsWith("@");
        }

        private void ProcessAdmin(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            if (source.Name != "xTr1m" && source.Name != "Concilium")
                return;

            if (parameters[0] == "say")
            {
                Client.LocalUser.SendMessage(parameters[1], string.Join(" ", parameters.Skip(2)));
            }
            else if (parameters[0] == "restart")
            {
                Disconnect();
                Connect();
            }
            else if (parameters[0] == "quit")
            {
                this.Save();
                Disconnect();
                System.Environment.Exit(0);
            }
            else if (parameters[0] == "topic")
            {
                client.GetChannel(parameters[1]).SetTopic(string.Join(" ", parameters.Skip(2)));
            }
        }

        private void ProcessChatRandom(IrcClient client, IIrcMessageSource source, IList<IIrcMessageTarget> targets, string message, IList<string> parameters)
        {
            if (parameters.Count != 2)
                throw new InvalidCommandParametersException(".random", 2);

            Log.Get().Info(string.Format("{0} requested a random number", source.Name));
            var min = double.Parse(parameters[0]);
            var max = double.Parse(parameters[1]);
            var len = max - min;
            var rand = new Random();
            Client.LocalUser.SendMessage(GetDefaultReplyTargets(client, source, targets), (Math.Floor((rand.NextDouble() * len + min) * 100.0) / 100.0).ToString());
        }

        public static string GetIP(string hostName)
        {
            try
            {
                var host = Dns.GetHostEntry(hostName);
                foreach (IPAddress ip in host.AddressList)
                {
                    if (ip.AddressFamily == AddressFamily.InterNetwork)
                    {
                        return ip.ToString();
                    }
                }
            }
            catch (Exception)
            {
                try
                {
                    var nics = NetworkInterface.GetAllNetworkInterfaces();
                    foreach (var nic in nics)
                    {
                        var ipProps = nic.GetIPProperties();

                        // We're only interested in IPv4 addresses for this example.
                        var ipv4Addrs = ipProps.UnicastAddresses
                          .Where(addr => addr.Address.AddressFamily == AddressFamily.InterNetwork);

                        foreach (var addr in ipv4Addrs)
                        {
                            if (addr.IPv4Mask == null)
                                continue;

                            return addr.Address.ToString();
                        }
                    }
                }
                catch (Exception)
                {
                }
            }

            return null;
        }

        public static WebClient CreateWebClient()
        {
            var wc = new WebClient();
            wc.Headers.Add(HttpRequestHeader.UserAgent, "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.11 (KHTML, like Gecko) Chrome/23.0.1271.64 Safari/537.11");
            wc.Headers.Add(HttpRequestHeader.AcceptLanguage, "de-DE,de;q=0.8,en-US;q=0.6,en;q=0.4");
            wc.Headers.Add(HttpRequestHeader.Referer, "91.194.90.37");
            return wc;
        }

    }
}
