/*
Technitium Bit Chat
Copyright (C) 2016  Shreyas Zare ([email protected])

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 3 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, see <http://www.gnu.org/licenses/>.

*/

using BitChatCore;
using BitChatCore.FileSharing;
using BitChatCore.Network.SecureChannel;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using TechnitiumLibrary.Security.Cryptography;

namespace BitChatApp.UserControls
{
    public partial class ChatMessageView : CustomPanel
    {
        #region events

        public event EventHandler SettingsModified;
        public event EventHandler ShareFile;

        #endregion

        #region variables

        const int MESSAGE_COUNT_PER_SCROLL = 20;

        BitChat _chat;
        ChatListItem _chatItem;

        bool _skipSettingsModifiedEvent = false;
        DateTime lastTypingNotification;

        #endregion

        #region constructor

        public ChatMessageView(BitChat chat, ChatListItem chatItem)
        {
            InitializeComponent();

            _chat = chat;
            _chatItem = chatItem;

            _chat.MessageReceived += chat_MessageReceived;
            _chat.MessageDeliveryNotification += chat_MessageDeliveryNotification;
            _chat.FileAdded += chat_FileAdded;
            _chat.PeerAdded += chat_PeerAdded;
            _chat.PeerTyping += chat_PeerTyping;
            _chat.PeerHasRevokedCertificate += chat_PeerHasRevokedCertificate;
            _chat.PeerSecureChannelException += chat_PeerSecureChannelException;
            _chat.PeerHasChangedCertificate += chat_PeerHasChangedCertificate;

            this.Title = _chat.NetworkDisplayTitle;

            if (_chat.NetworkType == BitChatCore.Network.BitChatNetworkType.GroupChat)
                _chat.GroupImageChanged += chat_GroupImageChanged;

            //load stored messages
            int totalMessageCount = _chat.GetMessageCount();
            if (totalMessageCount > 0)
            {
                try
                {
                    customListView1.ReplaceItems(ConvertToListViewItems(_chat.GetLastMessages(totalMessageCount, MESSAGE_COUNT_PER_SCROLL), true));
                    customListView1.ScrollToBottom();
                }
                catch
                { }
            }
        }

        #endregion

        #region bitchat events

        private void chat_GroupImageChanged(BitChat sender, BitChat.Peer peer)
        {
            _chat.WriteInfoMessage(peer.PeerCertificate.IssuedTo.Name + " changed group photo");
        }

        private void chat_MessageReceived(BitChat.Peer sender, MessageItem message)
        {
            if (message.Type == MessageType.Info)
            {
                AddMessage(new ChatMessageInfoItem(message), sender.IsSelf);
            }
            else
            {
                AddMessage(new ChatMessageTextItem(sender, message), sender.IsSelf);

                string msg = message.Message;

                if (msg.Length > 100)
                    msg = msg.Substring(0, 100);

                ShowPeerTypingNotification(sender.PeerCertificate.IssuedTo.Name, false);
            }
        }

        private void chat_MessageDeliveryNotification(BitChat.Peer sender, MessageItem message)
        {
            foreach (Control item in customListView1.Controls)
            {
                ChatMessageTextItem msgItem = item as ChatMessageTextItem;
                if ((msgItem != null) && (msgItem.Message.MessageNumber == message.MessageNumber))
                {
                    msgItem.DeliveryNotification(message);
                    break;
                }
            }
        }

        private void chat_FileAdded(BitChat.Peer peer, MessageItem message, SharedFile sharedFile)
        {
            ChatMessageFileItem fileItem = new ChatMessageFileItem(peer, message, sharedFile);
            AddMessage(fileItem, true);

            fileItem.ShareFile += FileItem_ShareFile;
        }

        private void chat_PeerAdded(BitChat sender, BitChat.Peer peer)
        {
            _chat.WriteInfoMessage(peer.PeerCertificate.IssuedTo.Name + " joined chat");

            if (sender.NetworkType == BitChatCore.Network.BitChatNetworkType.PrivateChat)
                this.Title = peer.PeerCertificate.IssuedTo.Name + " <" + peer.PeerCertificate.IssuedTo.EmailAddress.Address + ">";
        }

        private void chat_PeerTyping(BitChat sender, BitChat.Peer peer)
        {
            ShowPeerTypingNotification(peer.PeerCertificate.IssuedTo.Name, true);
        }

        private void chat_PeerHasRevokedCertificate(BitChat sender, InvalidCertificateException ex)
        {
            _chat.WriteInfoMessage(ex.Message);
        }

        private void chat_PeerSecureChannelException(BitChat sender, SecureChannelException ex)
        {
            string peerInfo;

            if (ex.PeerCertificate == null)
                peerInfo = "[" + ex.PeerEP.ToString() + "]";
            else
                peerInfo = ex.PeerCertificate.IssuedTo.Name + " <" + ex.PeerCertificate.IssuedTo.EmailAddress.Address + "> [" + ex.PeerEP.ToString() + "]";

            _chat.WriteInfoMessage("Secure channel with peer '" + peerInfo + "' encountered '" + ex.Code.ToString() + "' exception.");

            if (ex.InnerException != null)
                _chat.WriteInfoMessage(ex.InnerException.ToString());
        }

        private void chat_PeerHasChangedCertificate(BitChat sender, Certificate cert)
        {
            _chat.WriteInfoMessage("Warning! Peer '" + cert.IssuedTo.EmailAddress.Address + "' has changed profile certificate [serial number: " + cert.SerialNumber + ", issued on: " + cert.IssuedOnUTC.ToShortDateString() + "]");
        }

        #endregion

        #region protected UI code

        protected override void OnResize(EventArgs e)
        {
            _skipSettingsModifiedEvent = true;

            base.OnResize(e);

            if (splitContainer1 != null)
                txtMessage.Size = new Size(splitContainer1.Panel2.Width - 1 - 2 - btnSend.Width - 1, splitContainer1.Panel2.Height - 2);

            _skipSettingsModifiedEvent = false;
        }

        #endregion

        #region public

        public void SetFocusMessageEditor()
        {
            txtMessage.Focus();
        }

        public void TrimMessageList()
        {
            if (customListView1.IsScrolledToBottom())
                customListView1.TrimListFromTop(MESSAGE_COUNT_PER_SCROLL);
        }

        public void ReadSettingsFrom(BinaryReader bR)
        {
            _skipSettingsModifiedEvent = true;
            splitContainer1.SplitterDistance = splitContainer1.Height - bR.ReadInt32();
            _skipSettingsModifiedEvent = false;
        }

        public void WriteSettingsTo(BinaryWriter bW)
        {
            bW.Write(splitContainer1.Height - splitContainer1.SplitterDistance);
        }

        public void ShareFiles(string[] fileNames)
        {
            AddMessage(new ChatMessageInfoItem(new MessageItem("Please wait while file(s) are being shared")), true);

            Action<string[]> d = new Action<string[]>(ShareFileAsync);
            d.BeginInvoke(fileNames, null, null);
        }

        #endregion

        #region UI code

        private void FileItem_ShareFile(object sender, EventArgs e)
        {
            ShareFile?.Invoke(sender, e);
        }

        private void splitContainer1_SplitterMoved(object sender, SplitterEventArgs e)
        {
            if ((SettingsModified != null) && !_skipSettingsModifiedEvent)
                SettingsModified(this, EventArgs.Empty);
        }

        private void txtMessage_KeyPress(object sender, KeyPressEventArgs e)
        {
            if (e.KeyChar == '\r')
            {
                btnSend_Click(null, null);

                e.Handled = true;
                lastTypingNotification = DateTime.UtcNow.AddSeconds(-10);
            }
            else
            {
                DateTime current = DateTime.UtcNow;

                if ((current - lastTypingNotification).TotalSeconds > 5)
                {
                    lastTypingNotification = current;
                    _chat.SendTypingNotification();
                }
            }
        }

        private void txtMessage_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Control)
            {
                switch (e.KeyCode)
                {
                    case Keys.V:
                        if (Clipboard.ContainsFileDropList())
                        {
                            List<string> fileNames = new List<string>();

                            foreach (string filePath in Clipboard.GetFileDropList())
                            {
                                if (File.Exists(filePath))
                                    fileNames.Add(filePath);
                            }

                            ShareFiles(fileNames.ToArray());
                            e.Handled = true;
                            e.SuppressKeyPress = true;
                        }
                        break;

                    case Keys.Back:
                        string msgRight = txtMessage.Text.Substring(0, txtMessage.SelectionStart);
                        string msgLeft = txtMessage.Text.Substring(txtMessage.SelectionStart);

                        int i = msgRight.TrimEnd().LastIndexOfAny(new char[] { ' ', '\n' });

                        if (i > -1)
                        {
                            i++;
                            txtMessage.Text = msgRight.Substring(0, i) + msgLeft;
                            txtMessage.SelectionStart = i;
                        }
                        else
                        {
                            txtMessage.Text = msgLeft;
                            txtMessage.SelectionStart = 0;
                        }

                        e.Handled = true;
                        e.SuppressKeyPress = true;
                        break;
                }
            }
        }

        private void btnSend_Click(object sender, EventArgs e)
        {
            if (txtMessage.Text != "")
            {
                _chat.SendTextMessage(txtMessage.Text);

                txtMessage.Text = "";
                txtMessage.Focus();
            }
        }

        private void btnShareFile_Click(object sender, EventArgs e)
        {
            using (OpenFileDialog oFD = new OpenFileDialog())
            {
                oFD.Title = "Select files to share ...";
                oFD.CheckFileExists = true;
                oFD.Multiselect = true;

                if (oFD.ShowDialog(this) == DialogResult.OK)
                {
                    ShareFiles(oFD.FileNames);
                }
            }
        }

        private List<CustomListViewItem> ConvertToListViewItems(MessageItem[] items, bool updateLastMessageInChatItem)
        {
            BitChat.Peer[] peerList = _chat.GetPeerList();
            SharedFile[] fileList = _chat.GetSharedFileList();

            List<CustomListViewItem> listItems = new List<CustomListViewItem>(items.Length);
            DateTime lastItemDate = new DateTime();
            string lastMessage = null;
            DateTime lastMessageDate = new DateTime();

            foreach (MessageItem item in items)
            {
                if (lastItemDate.Date < item.MessageDate.Date)
                {
                    lastItemDate = item.MessageDate;
                    listItems.Add(new ChatMessageInfoItem(new MessageItem(lastItemDate)));
                }

                switch (item.Type)
                {
                    case MessageType.Info:
                        listItems.Add(new ChatMessageInfoItem(item));
                        break;

                    case MessageType.TextMessage:
                    case MessageType.InvitationMessage:
                        {
                            BitChat.Peer sender = null;

                            foreach (BitChat.Peer peer in peerList)
                            {
                                if (peer.PeerCertificate.IssuedTo.EmailAddress.Address.Equals(item.Sender))
                                {
                                    sender = peer;
                                    break;
                                }
                            }

                            listItems.Add(new ChatMessageTextItem(sender, item));

                            if (sender == null)
                                lastMessage = item.Sender + ": " + item.Message;
                            else if (sender.IsSelf)
                                lastMessage = item.Message;
                            else
                                lastMessage = sender.PeerCertificate.IssuedTo.Name + ": " + item.Message;

                            lastMessageDate = item.MessageDate;
                        }
                        break;

                    case MessageType.SharedFileMetaData:
                        {
                            BitChat.Peer sender = null;
                            SharedFile file = null;

                            foreach (BitChat.Peer peer in peerList)
                            {
                                if (peer.PeerCertificate.IssuedTo.EmailAddress.Address.Equals(item.Sender))
                                {
                                    sender = peer;
                                    break;
                                }
                            }

                            foreach (SharedFile sharedFile in fileList)
                            {
                                if (sharedFile.MetaData.FileID.Equals(item.SharedFileMetaData.FileID))
                                {
                                    file = sharedFile;
                                    break;
                                }
                            }

                            ChatMessageFileItem fileItem = new ChatMessageFileItem(sender, item, file);
                            listItems.Add(fileItem);

                            fileItem.ShareFile += FileItem_ShareFile;

                            if (sender == null)
                                lastMessage = item.Sender + " shared a file";
                            else if (sender.IsSelf)
                                lastMessage = "file was shared";
                            else
                                lastMessage = sender.PeerCertificate.IssuedTo.Name + " shared a file";

                            lastMessageDate = item.MessageDate;
                        }
                        break;
                }
            }

            if (updateLastMessageInChatItem && (lastMessage != null))
                _chatItem.SetLastMessage(lastMessage, lastMessageDate, false);

            return listItems;
        }

        private void customListView1_ScrolledNearStart(object sender, EventArgs e)
        {
            foreach (CustomListViewItem item in customListView1.Controls)
            {
                IChatMessageItem messageItem = item as IChatMessageItem;

                if (messageItem.Message.MessageNumber == 0)
                {
                    return;
                }
                else if (messageItem.Message.MessageNumber > -1)
                {
                    customListView1.InsertItemsAtTop(ConvertToListViewItems(_chat.GetLastMessages(messageItem.Message.MessageNumber, MESSAGE_COUNT_PER_SCROLL), false));
                    return;
                }
            }
        }

        private void ShowPeerTypingNotification(string peerName, bool add)
        {
            lock (timerTypingNotification)
            {
                List<string> peerNames;

                if (labTypingNotification.Tag == null)
                {
                    peerNames = new List<string>(3);
                    labTypingNotification.Tag = peerNames;
                }
                else
                {
                    peerNames = labTypingNotification.Tag as List<string>;
                }

                {
                    if (add)
                    {
                        if (!peerNames.Contains(peerName))
                            peerNames.Add(peerName);
                    }
                    else
                    {
                        if (peerName == null)
                            peerNames.Clear();
                        else
                            peerNames.Remove(peerName);
                    }
                }

                switch (peerNames.Count)
                {
                    case 0:
                        labTypingNotification.Text = "";
                        break;

                    case 1:
                        labTypingNotification.Text = peerNames[0] + " is typing...";
                        break;

                    case 2:
                        labTypingNotification.Text = peerNames[0] + " and " + peerNames[1] + " are typing...";
                        break;

                    case 3:
                        labTypingNotification.Text = peerNames[0] + ", " + peerNames[1] + " and " + peerNames[2] + " are typing...";
                        break;

                    default:
                        labTypingNotification.Text = "many people are typing...";
                        break;
                }

                if (peerName != null)
                {
                    timerTypingNotification.Stop();
                    timerTypingNotification.Start();
                }
            }
        }

        private void timerTypingNotification_Tick(object sender, EventArgs e)
        {
            lock (timerTypingNotification)
            {
                timerTypingNotification.Stop();

                ShowPeerTypingNotification(null, false);
            }
        }

        private void ShareFileAsync(string[] filenames)
        {
            foreach (string filename in filenames)
            {
                try
                {
                    _chat.ShareFile(filename);
                }
                catch (Exception ex)
                {
                    MessageBox.Show("Error encountered while sharing file.\r\n\r\n" + ex.Message, "Error!", MessageBoxButtons.OK, MessageBoxIcon.Error);
                }
            }
        }

        private void AddMessage(CustomListViewItem item, bool selfSender)
        {
            CustomListViewItem lastItem = customListView1.GetLastItem();

            bool insertDateInfo = false;
            DateTime itemDate = (item as IChatMessageItem).Message.MessageDate;

            if (lastItem == null)
            {
                insertDateInfo = true;
            }
            else
            {
                if (itemDate.Date > (lastItem as IChatMessageItem).Message.MessageDate.Date)
                    insertDateInfo = true;
            }

            bool wasScrolledToBottom = customListView1.IsScrolledToBottom();

            if (insertDateInfo)
                customListView1.AddItem(new ChatMessageInfoItem(new MessageItem(DateTime.UtcNow)));

            customListView1.AddItem(item);

            if (_chatItem.Selected && (wasScrolledToBottom || selfSender))
                customListView1.ScrollToBottom();
        }

        #endregion
    }
}