// Smuxi - Smart MUltipleXed Irc
// 
// Copyright (c) 2012 Carlos Martín Nieto <cmn@dwim.me>
// 
// Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
// 
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
using System;
using System.Web;
using System.Net;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Threading;
using Smuxi.Common;
using Smuxi.Engine.Campfire;
using ServiceStack.ServiceClient.Web;

namespace Smuxi.Engine
{
    [ProtocolManagerInfo(Name = "Campfire", Description = "Campfire chat", Alias = "campfire")]
    public class CampfireProtocolManager : ProtocolManagerBase
    {
        static readonly string f_LibraryTextDomain = "smuxi-engine-campfire";

        Dictionary<ChatModel, CampfireEventStream> EventStreams { get; set; }
        int LastSentId { get; set; }
        IEnumerable<Room> Rooms { get; set; }
        DateTime RoomsUpdated { get; set; }
        TimeSpan RefreshInterval { get; set; }
        Dictionary<int, CampfirePersonModel>  Users { get; set; }
        string Key { get; set; }
        string Network { get; set; }
        Uri BaseUri { get; set; }
        ChatModel NetworkChat { get; set; }
        JsonServiceClient Client { get; set; }

        public override string Protocol {
            get {
                return "Campfire";
            }
        }

        public override string NetworkID {
            get {
                return Network;
            }
        }

        public override ChatModel Chat {
            get {
                return NetworkChat;
            }
        }

        static CampfireProtocolManager()
        {
        }

        private CampfirePersonModel CreatePerson(User user)
        {
            var person = new CampfirePersonModel(user, NetworkID, this);
            return person;
        }

        private void GetUserDetails(int id)
        {
            if (Users.ContainsKey(id) || id == 0)
                return;

            var u = Client.Get<UserResponse>(String.Format("/users/{0}.json", id)).User;
            Users[u.Id] = CreatePerson(u);
        }

        private void RefreshRooms()
        {
            if (Rooms == null ||
                RefreshInterval.CompareTo(RoomsUpdated - DateTime.Now) > 0)
                Rooms = Client.Get<RoomsResponse>("/rooms.json").Rooms;
        }

        public CampfireProtocolManager(Session session) : base(session)
        {
            Trace.Call(session);
            RefreshInterval = TimeSpan.FromMinutes(5);
            RoomsUpdated = DateTime.MinValue;
            Users = new Dictionary<int, CampfirePersonModel>();
            EventStreams = new Dictionary<ChatModel, CampfireEventStream>();
        }

        private void FailedToConnect(string str, Exception e)
        {
            Session.AddMessageToChat(NetworkChat, CreateMessageBuilder()
                                     .AppendErrorText("{0}: {1}", str, e.Message)
                                     .ToMessage());
        }

        public override void Connect(FrontendManager fm, ServerModel server)
        {
            Trace.Call(fm, server);

            Network = server.Hostname.Substring(0, server.Hostname.IndexOf('.'));
            Host = server.Hostname;
            BaseUri = new Uri(String.Format("https://{0}", Host));

            NetworkChat = new ProtocolChatModel(Network, "Campfire " + Network, this);
            NetworkChat.InitMessageBuffer(MessageBufferPersistencyType.Volatile);
            NetworkChat.ApplyConfig(Session.UserConfig);
            Session.AddChat(NetworkChat);
            Session.SyncChat(NetworkChat);
            var msg = _("Connecting to campfire... ");
            if (fm != null) {
                fm.SetStatus(msg);
            }
            var bld = CreateMessageBuilder().AppendEventPrefix().AppendText(msg);
            Session.AddMessageToChat(NetworkChat, bld.ToMessage());

            if (!server.ValidateServerCertificate) {
                var whitelist = Session.CertificateValidator.HostnameWhitelist;
                lock (whitelist) {
                    // needed for favicon
                    if (!whitelist.Contains("campfirenow.com")) {
                        whitelist.Add("campfirenow.com");
                    }
                    // needed for receiving messages
                    if (!whitelist.Contains("streaming.campfirenow.com")) {
                        whitelist.Add("streaming.campfirenow.com");
                    }
                    if (!whitelist.Contains(Host)) {
                        whitelist.Add(Host);
                    }
                }
            }

            Client = new JsonServiceClient(BaseUri.AbsoluteUri);
            var creds = new NetworkCredential(server.Username, server.Password);
            Client.Credentials = creds;

            try {
                var me = Client.Get<UserResponse>("/users/me.json").User;
                Key = me.Api_Auth_Token;
                Me = CreatePerson(me);
                // The blue color is hardcoded for now
                Me.IdentityNameColored.ForegroundColor = new TextColor(0x0000FF);
                Me.IdentityNameColored.BackgroundColor = TextColor.None;
                Me.IdentityNameColored.Bold = true;

            } catch (Exception e) {
                FailedToConnect("Failed to connect to Campfire", e);
                return;
            }

            Client.Credentials = new NetworkCredential(Key, "X");
            msg = _("Connected to campfire");
            if (fm != null) {
                fm.SetStatus(msg);
            }
            bld = CreateMessageBuilder().AppendEventPrefix().AppendText(msg);
            Session.AddMessageToChat(NetworkChat, bld.ToMessage());

            // Campfire lets us know what channels the user is currently in, so
            // connect to those rooms automatically
            Rooms = Client.Get<RoomsResponse>("/rooms.json").Rooms;
            RoomsUpdated = DateTime.Now;

            var myrooms = Client.Get<RoomsResponse>("/presence.json").Rooms;
            if (myrooms.Length > 0) {
                bld = CreateMessageBuilder().
                    AppendEventPrefix().
                    AppendText("Present in {0}",
                        String.Join(", ", myrooms.Select(r => r.Name).ToArray())
                    );
                Session.AddMessageToChat(NetworkChat, bld.ToMessage());
            }

            foreach (var room in myrooms) {
                var chat = new GroupChatModel(room.Id.ToString(), room.Name, null);
                OpenChat(fm, chat);
            }
        }

        public void CommandHelp(CommandModel cd)
        {
            var builder = CreateMessageBuilder();
            builder.AppendEventPrefix();
            // TRANSLATOR: this line is used as a label / category for a
            // list of commands below
            builder.AppendHeader(_("Campfire Commands"));
            Session.AddMessageToFrontend(cd, builder.ToMessage());

            string[] help = {
                "connect campfire username password",
                "join",
                "topic",
                "uploads",
                "say"
            };

            foreach (string line in help) {
                builder = CreateMessageBuilder();
                builder.AppendEventPrefix();
                builder.AppendText(line);
                Session.AddMessageToFrontend(cd, builder.ToMessage());
            }
        }


        public void CommandJoin(CommandModel cmd)
        {
            Trace.Call(cmd);

            RefreshRooms();

            /*
             * cmd.DataArray is split at SP, but that's an allowed character
             * for Campfire. Instead of relying on that, we need to remove the "/join "
             * part and then split on ','
             */
            var chans = cmd.Parameter.Split(',');
            var list = Rooms.Where(r => chans.Any(r.Name.Equals));

            foreach(Room room in list) {
                var chat = new GroupChatModel(room.Id.ToString(), room.Name, null);
                OpenChat(cmd.FrontendManager, chat);
            }

        }

        public void CommandTopic(CommandModel cmd)
        {
            Trace.Call(cmd);

            var update = new UpdateTopicWrapper {
                room = new TopicChange {
                    topic = cmd.Parameter
                }
            };

            Client.Put<object>(String.Format("/room/{0}.json", cmd.Chat.ID), update);
        }

        public void CommandUploads(CommandModel cmd)
        {
            Trace.Call(cmd);

            var uploads = Client.Get<UploadsResponse>(String.Format("/room/{0}/uploads.json", cmd.Chat.ID)).Uploads;

            foreach (var upload in uploads) {
                var bld = CreateMessageBuilder();
                bld.AppendEventPrefix().AppendHeader(_("Upload")).AppendSpace();
                bld.AppendText(_("'{0}' ({1} B) {2}"), upload.Name, upload.Byte_Size, upload.Full_Url);
                Session.AddMessageToChat(cmd.Chat, bld.ToMessage());
            }
        }

        public void CommandSay(CommandModel cmd)
        {
            Trace.Call(cmd);
            SendMessage((GroupChatModel) cmd.Chat, cmd.Parameter);
        }

        public override bool Command(CommandModel command)
        {
            Trace.Call(command);

            bool handled = false;

            switch (command.Command) {
                case "j":
                case "join":
                    CommandJoin(command);
                    handled = true;
                    break;
                case "say":
                    CommandSay(command);
                    handled = true;
                    break;
                case "help":
                    CommandHelp(command);
                    handled = true;
                    break;
                case "topic":
                    CommandTopic(command);
                    handled = true;
                    break;
                case "uploads":
                    CommandUploads(command);
                    handled = true;
                    break;
                default: // nothing, normal chat
                    handled = true;
                    if (command.Chat is GroupChatModel)
                        SendMessage((GroupChatModel) command.Chat, command.Data);
                    break;
            }

            return handled;
        }

        public override IList<GroupChatModel> FindGroupChats(GroupChatModel filter)
        {
            Trace.Call(filter);

            RefreshRooms();
            string searchPattern = null;
            if (filter == null || String.IsNullOrEmpty(filter.Name)) {
                // full channel list
            } else {
                if (!filter.Name.StartsWith("*") && !filter.Name.EndsWith("*")) {
                    searchPattern = String.Format("*{0}*", filter.Name);
                } else {
                    searchPattern = filter.Name;
                }
            }

            List<GroupChatModel> chats = new List<GroupChatModel>(Rooms.Count());
            IEnumerable<Room> matching;

            matching = searchPattern == null ? Rooms :
                Rooms.Where(r => Pattern.IsMatch(r.Name, searchPattern));

            foreach (var room in matching) {
                GroupChatModel chat = new GroupChatModel(room.Id.ToString(), room.Name, null);
                var users = Client.Get<RoomResponse>(String.Format("/room/{0}.json", chat.ID)).Room.Users;

                /* Don't waste this data */
                lock (Users) {
                    foreach (var user in users) {
                        if (!Users.ContainsKey(user.Id))
                            Users[user.Id] = CreatePerson(user);
                    }
                }


                chat.PersonCount = users.Length;

                chat.Topic = CreateMessageBuilder().AppendMessage(room.Topic).ToMessage();
                lock (chat) {
                    chats.Add(chat);
                }
            }

            return chats;
        }

        void SendMessage(GroupChatModel chat, string text)
        {
            var message = new MessageSending { body = text };
            var wrapper = new MessageWrapper { message = message };
            Message res;
            try {
                res = Client.Post<MessageResponse>(String.Format("/room/{0}/speak.json", chat.ID), wrapper).Message;
            } catch (WebServiceException e) {
                var bld = CreateMessageBuilder();
                bld.AppendErrorText(_("Failed to post message: {0}"), e.Message);
                Session.AddMessageToChat(NetworkChat, bld.ToMessage());
                return;
            }

            ShowMessage(this, new MessageReceivedEventArgs(chat, res));
            LastSentId = res.Id;
        }

        void FormatUpload(MessageBuilder bld, PersonModel person, ChatModel chat, Message message)
        {
            // Figure out what the user uploaded, we need to issue another call for this
            var upload = Client.Get<UploadWrapper>(String.Format("/room/{0}/messages/{1}/upload.json", chat.ID, message.Id)).Upload;

            bld.AppendEventPrefix();
            bld.AppendIdendityName(person).AppendSpace();
            bld.AppendText(_("has uploaded '{0}' ({1} B) {2}"), upload.Name, upload.Byte_Size, upload.Full_Url);
        }

        void FormatEvent(MessageBuilder bld, PersonModel person, string action)
        {
            bld.AppendEventPrefix();
            bld.AppendIdendityName(person).AppendSpace();
            bld.AppendText(action);
        }

        bool AlreadyPosted(PersonModel person, int messageId)
        {
            return person == Me && messageId <= LastSentId;
        }

        void ShowMessage(object sender, MessageReceivedEventArgs args)
        {
            var message = args.Message;
            var chat = args.Chat;
            bool processed = true;

            if (message.Type == Campfire.MessageType.TimestampMessage)
                return;


            CampfirePersonModel person;
            lock (Users) {
                GetUserDetails(message.User_Id); /* Make sure we know who this is */
                person = Users[message.User_Id];
            }

            var bld = CreateMessageBuilder();
            bld.TimeStamp = message.Created_At.DateTime;

            switch (message.Type) {
                case Campfire.MessageType.EnterMessage:
                    // TRANSLATOR: {0} is the name of the room
                    FormatEvent(bld, person, String.Format(_("has joined {0}"), chat.Name));
                    lock (chat) {
                        if (chat.GetPerson(person.ID) == null)
                            Session.AddPersonToGroupChat(chat, person);
                    }
                    break;
                case Campfire.MessageType.KickMessage:
                case Campfire.MessageType.LeaveMessage:
                    // TRANSLATOR: {0} is the name of the room
                    FormatEvent(bld, person, String.Format(_("has left {0}"), chat.Name));
                    lock (chat) {
                        if (chat.GetPerson(person.ID) != null)
                            Session.RemovePersonFromGroupChat(chat, person);
                    }
                    break;
                case Campfire.MessageType.LockMessage:
                    // TRANSLATOR: {0} is the name of the room
                    FormatEvent(bld, person, String.Format(_("has locked {0}"), chat.Name));
                    break;
                case Campfire.MessageType.UnlockMessage:
                    // TRANSLATOR: {0} is the name of the room
                    FormatEvent(bld, person, String.Format(_("has unlocked {0}"), chat.Name));
                    break;
                case Campfire.MessageType.TopicChangeMessage:
                    var topic = CreateMessageBuilder().AppendMessage(message.Body);
                    Session.UpdateTopicInGroupChat(chat, topic.ToMessage());
                    FormatEvent(bld, person, _("has changed the topic"));
                    break;
                case Campfire.MessageType.UploadMessage:
                    FormatUpload(bld, person, chat, message);
                    break;
                case Campfire.MessageType.TweetMessage:
                    if (AlreadyPosted(person, message.Id))
                        return;

                    var tweet = message.Tweet;
                    // TRANSLATOR: {0} is the twitter username, {1} the tweet text
                    FormatEvent(bld, person,
                        String.Format(_("has pasted a tweet by {0}: {1}"), tweet.Author_Username, tweet.Message));
                    break;
                case Campfire.MessageType.TextMessage:
                case Campfire.MessageType.PasteMessage:
                    processed = false;
                    break;
                default:
                    FormatEvent(bld, person, String.Format(_("has performed an unknown action"), chat.Name));
                    break;
            }

            if (processed) {
                Session.AddMessageToChat(chat, bld.ToMessage());
                return;
            }

            if (AlreadyPosted(person, message.Id))
                return;

            bool mine = person == Me;

            if (mine)
                bld.AppendSenderPrefix(Me);
            else
                bld.AppendNick(person).AppendSpace();

            if (message.Type == Campfire.MessageType.TextMessage) {
                bld.AppendMessage(message.Body);
            } else if (message.Type == Campfire.MessageType.PasteMessage) {
                bld.AppendText("\n");
                foreach (string part in message.Body.Split('\n')) {
                    bld.AppendText("    {0}\n", part);
                }
            }

            if (!mine)
                bld.MarkHighlights();

            Session.AddMessageToChat(chat, bld.ToMessage());
    }

        void ShowError(object sender, ErrorReceivedEventArgs args)
        {
            var message = args.StatusDescription;
            var bld = CreateMessageBuilder();

            bld.AppendErrorText(_("Error reading from stream: {0}"), message);
            Session.AddMessageToChat(NetworkChat, bld.ToMessage());
        }

        public override void OpenChat(FrontendManager fm, ChatModel chat_)
        {
            Trace.Call(fm, chat_);

            var room = Rooms.Single(r => r.Name.Equals(chat_.Name));
            Client.Post<object>(String.Format("/room/{0}/join.json", room.Id), null);
            room = Client.Get<RoomResponse>(String.Format("/room/{0}.json", room.Id)).Room;
            var chat = Session.GetChat(room.Name, ChatType.Group, this) as GroupChatModel;
            if (chat == null)
                chat = Session.CreateChat<GroupChatModel>(room.Id.ToString(), room.Name, this);

            var bld = CreateMessageBuilder();
            bld.AppendMessage(room.Topic);
            chat.Topic = bld.ToMessage();

            Session.AddChat(chat);

            /* Fill what we know about the users, this is only the currently-connected ones */
            lock (Users) {
                foreach (User user in room.Users) {
                    if (!Users.ContainsKey(user.Id))
                        Users[user.Id] = CreatePerson(user);
                    Session.AddPersonToGroupChat(chat, Users[user.Id]);
                }
            }

            /* Show the recent messages, then go live. FIXME: race condition */
            var recent = Client.Get<MessagesResponse>(String.Format("/room/{0}/recent.json", chat.ID)).Messages;
            foreach (Message message in recent)
                ShowMessage(this, new MessageReceivedEventArgs(chat, message));

            Session.SyncChat(chat);
            chat.IsSynced = true; // Let the part and join messages take affect

            var stream = new CampfireEventStream(chat, BaseUri, new NetworkCredential(Key, "X"));
            lock (EventStreams)
                EventStreams.Add(chat, stream);

            stream.MessageReceived += ShowMessage;
            stream.ErrorReceived += ShowError;
            stream.Start();
        }

        public override void CloseChat(FrontendManager fm, ChatModel ChatInfo)
        {
            var chat = GetChat(ChatInfo.ID, ChatType.Group);
            Client.Post<object>(String.Format("/room/{0}/leave.json", chat.ID), null);
            Session.RemoveChat(chat);
            lock (EventStreams) {
                var stream = EventStreams[chat];
                stream.Dispose();
                EventStreams.Remove(chat);
            }
        }

        public override void Reconnect(FrontendManager fm)
        {
            Trace.Call(fm);
        }

        public override void Disconnect(FrontendManager fm)
        {
            Trace.Call(fm);
        }

        public override void SetPresenceStatus(PresenceStatus status, string message)
        {
        }

        public override void Dispose()
        {
            Trace.Call();

            lock (EventStreams) {
                foreach (var stream in EventStreams.Values)
                    stream.Dispose();
            }

            base.Dispose();
        }

        public override string ToString()
        {
            return Network;
        }

        private static string _(string msg)
        {
            return LibraryCatalog.GetString(msg, f_LibraryTextDomain);
        }

    }
}

