/*
* Copyright (c) Contributors, http://opensimulator.org/
* See CONTRIBUTORS.TXT for a full list of copyright holders.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*     * Redistributions of source code must retain the above copyright
*       notice, this list of conditions and the following disclaimer.
*     * Redistributions in binary form must reproduce the above copyright
*       notice, this list of conditions and the following disclaimer in the
*       documentation and/or other materials provided with the distribution.
*     * Neither the name of the OpenSim Project nor the
*       names of its contributors may be used to endorse or promote products
*       derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE DEVELOPERS AS IS AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
* 
*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using System.Threading;
using libsecondlife;
using Nini.Config;
using OpenSim.Framework;
using OpenSim.Framework.Console;
using OpenSim.Region.Environment.Interfaces;
using OpenSim.Region.Environment.Scenes;

namespace OpenSim.Region.Environment.Modules
{
    public class ChatModule : IRegionModule, ISimChat
    {
        private List<Scene> m_scenes = new List<Scene>();
        private LogBase m_log;

        private int m_whisperdistance = 10;
        private int m_saydistance = 30;
        private int m_shoutdistance = 100;

        private IRCChatModule m_irc = null;

        public ChatModule()
        {
            m_log = MainLog.Instance;
        }

        public void Initialise(Scene scene, IConfigSource config)
        {
            // wrap this in a try block so that defaults will work if
            // the config file doesn't specify otherwise.
            try
            {
                m_whisperdistance = config.Configs["Chat"].GetInt("whisper_distance", m_whisperdistance);
                m_saydistance = config.Configs["Chat"].GetInt("say_distance", m_saydistance);
                m_shoutdistance = config.Configs["Chat"].GetInt("shout_distance", m_shoutdistance);
            }
            catch (Exception)
            {
            }

            if (!m_scenes.Contains(scene))
            {
                m_scenes.Add(scene);
                scene.EventManager.OnNewClient += NewClient;
                scene.RegisterModuleInterface<ISimChat>(this);
            }

            // setup IRC Relay
            m_irc = new IRCChatModule(config);
        }

        public void PostInitialise()
        {
            if (m_irc.Enabled)
            {
                m_irc.Connect(m_scenes);
            }
        }

        public void Close()
        {
            m_irc.Close();
        }

        public string Name
        {
            get { return "ChatModule"; }
        }

        public bool IsSharedModule
        {
            get { return true; }
        }

        public void NewClient(IClientAPI client)
        {
            client.OnChatFromViewer += SimChat;
        }

        private void TrySendChatMessage(ScenePresence presence, LLVector3 fromPos, LLVector3 regionPos,
                                        LLUUID fromAgentID, string fromName, ChatTypeEnum type, string message)
        {
            if (!presence.IsChildAgent)
            {
                LLVector3 fromRegionPos = fromPos + regionPos;
                LLVector3 toRegionPos = presence.AbsolutePosition + regionPos;
                int dis = Math.Abs((int) Util.GetDistanceTo(toRegionPos, fromRegionPos));

                if (type == ChatTypeEnum.Whisper && dis > m_whisperdistance ||
                    type == ChatTypeEnum.Say && dis > m_saydistance ||
                    type == ChatTypeEnum.Shout && dis > m_shoutdistance)
                {
                    return;
                }

                // TODO: should change so the message is sent through the avatar rather than direct to the ClientView
                presence.ControllingClient.SendChatMessage(message, (byte) type, fromPos, fromName, fromAgentID);
            }
        }

        public void SimChat(Object sender, ChatFromViewerArgs e)
        {
            ScenePresence avatar = null;

            //TODO: Move ForEachScenePresence and others into IScene.
            Scene scene = (Scene) e.Scene;

            //TODO: Remove the need for this check
            if (scene == null)
                scene = m_scenes[0];

            // Filled in since it's easier than rewriting right now.
            LLVector3 fromPos = e.Position;
            LLVector3 regionPos = new LLVector3(scene.RegionInfo.RegionLocX*256, scene.RegionInfo.RegionLocY*256, 0);

            string fromName = e.From;
            string message = e.Message;
            LLUUID fromAgentID = LLUUID.Zero;

            if (e.Sender != null)
            {
                avatar = scene.GetScenePresence(e.Sender.AgentId);
            }

            if (avatar != null)
            {
                fromPos = avatar.AbsolutePosition;
                regionPos = new LLVector3(scene.RegionInfo.RegionLocX*256, scene.RegionInfo.RegionLocY*256, 0);
                fromName = avatar.Firstname + " " + avatar.Lastname;
                fromAgentID = e.Sender.AgentId;
            }

            if (e.Message.Length > 0)
            {
                if (m_irc.Connected && (avatar != null)) // this is to keep objects from talking to IRC
                {
                    m_irc.PrivMsg(fromName, scene.RegionInfo.RegionName, e.Message);
                }
            }

            if (e.Channel == 0)
            {
                foreach (Scene s in m_scenes)
                {
                    s.ForEachScenePresence(delegate(ScenePresence presence)
                                               {
                                                   TrySendChatMessage(presence, fromPos, regionPos,
                                                                      fromAgentID, fromName, e.Type, message);
                                               });
                }
            }
        }
    }

    internal class IRCChatModule
    {
        private string m_server = null;
        private uint m_port = 6668;
        private string m_user = "USER OpenSimBot 8 * :I'm a OpenSim to irc bot";
        private string m_nick = null;
        private string m_channel = null;

        private NetworkStream m_stream;
        private TcpClient m_tcp;
        private StreamWriter m_writer;
        private StreamReader m_reader;

        private Thread pingSender;
        private Thread listener;

        private bool m_enabled = false;
        private bool m_connected = false;

        private List<Scene> m_scenes = null;
        private LogBase m_log;

        public IRCChatModule(IConfigSource config)
        {
            m_nick = "OSimBot" + Util.RandomClass.Next(1, 99);
            m_tcp = null;
            m_writer = null;
            m_reader = null;

            try
            {
                m_server = config.Configs["IRC"].GetString("server");
                m_nick = config.Configs["IRC"].GetString("nick");
                m_channel = config.Configs["IRC"].GetString("channel");
                m_port = (uint) config.Configs["IRC"].GetInt("port", (int) m_port);
                m_user = config.Configs["IRC"].GetString("username", m_user);
                if (m_server != null && m_nick != null && m_channel != null)
                {
                    m_enabled = true;
                }
            }
            catch (Exception)
            {
                MainLog.Instance.Verbose("CHAT", "No IRC config information, skipping IRC bridge configuration");
            }
            m_log = MainLog.Instance;
        }

        public bool Connect(List<Scene> scenes)
        {
            try
            {
                m_scenes = scenes;

                m_tcp = new TcpClient(m_server, (int) m_port);
                m_log.Verbose("IRC", "Connecting...");
                m_stream = m_tcp.GetStream();
                m_log.Verbose("IRC", "Connected to " + m_server);
                m_reader = new StreamReader(m_stream);
                m_writer = new StreamWriter(m_stream);

                pingSender = new Thread(new ThreadStart(PingRun));
                pingSender.Start();

                listener = new Thread(new ThreadStart(ListenerRun));
                listener.Start();

                m_writer.WriteLine(m_user);
                m_writer.Flush();
                m_writer.WriteLine("NICK " + m_nick);
                m_writer.Flush();
                m_writer.WriteLine("JOIN " + m_channel);
                m_writer.Flush();
                m_log.Verbose("IRC", "Connection fully established");
                m_connected = true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
            }
            return m_connected;
        }

        public bool Enabled
        {
            get { return m_enabled; }
        }

        public bool Connected
        {
            get { return m_connected; }
        }

        public void PrivMsg(string from, string region, string msg)
        {
            try
            {
                m_writer.WriteLine("PRIVMSG {0} :<{1} in {2}>: {3}", m_channel, from, region, msg);
                m_writer.Flush();
            }
            catch (IOException)
            {
                m_log.Error("IRC", "Disconnected from IRC server.");
                listener.Abort();
                pingSender.Abort();
                m_connected = false;
            }
        }

        private Dictionary<string, string> ExtractMsg(string input)
        {
            Dictionary<string, string> result = null;
            string regex = @":(?<nick>\w*)!~(?<user>\S*) PRIVMSG (?<channel>\S+) :(?<msg>.*)";
            Regex RE = new Regex(regex, RegexOptions.Multiline);
            MatchCollection matches = RE.Matches(input);
            // Get some direct matches $1 $4 is a 
            if ((matches.Count == 1) && (matches[0].Groups.Count == 5))
            {
                result = new Dictionary<string, string>();
                result.Add("nick", matches[0].Groups[1].Value);
                result.Add("user", matches[0].Groups[2].Value);
                result.Add("channel", matches[0].Groups[3].Value);
                result.Add("msg", matches[0].Groups[4].Value);
            }
            else
            {
                m_log.Verbose("IRC", "Number of matches: " + matches.Count);
                if (matches.Count > 0)
                {
                    m_log.Verbose("IRC", "Number of groups: " + matches[0].Groups.Count);
                }
            }
            return result;
        }

        public void PingRun()
        {
            while (true)
            {
                m_writer.WriteLine("PING :" + m_server);
                m_writer.Flush();
                Thread.Sleep(15000);
            }
        }

        public void ListenerRun()
        {
            string inputLine;
            LLVector3 pos = new LLVector3(128, 128, 20);
            while (true)
            {
                while ((inputLine = m_reader.ReadLine()) != null)
                {
                    // Console.WriteLine(inputLine);
                    if (inputLine.Contains(m_channel))
                    {
                        Dictionary<string, string> data = ExtractMsg(inputLine);
                        if (data != null)
                        {
                            foreach (Scene m_scene in m_scenes)
                            {
                                m_scene.ForEachScenePresence(delegate(ScenePresence avatar)
                                                                 {
                                                                     if (!avatar.IsChildAgent)
                                                                     {
                                                                         avatar.ControllingClient.SendChatMessage(
                                                                             Helpers.StringToField(data["msg"]), 255,
                                                                             pos, data["nick"],
                                                                             LLUUID.Zero);
                                                                     }
                                                                 });
                            }
                        }
                    }
                }
                Thread.Sleep(50);
            }
        }

        public void Close()
        {
            listener.Abort();
            pingSender.Abort();
            m_writer.Close();
            m_reader.Close();
            m_tcp.Close();
        }
    }
}