using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;

using System.Xml.Serialization;
using System.Net.Sockets;
using System.Windows.Threading;
using System.Xml;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Globalization;
using System.Windows;
using System.Windows.Documents;

namespace MUDClientEssentials
{
    public class MUDServerConnection
    {
        //.NET's TCP Client connection, and a buffer to temporarily hold incoming/outgoing text
        private TcpClient connection = new TcpClient();
        private byte[] buffer = new byte[2500];
                       
        //disconnection callback and handler definition
        public event disconnectionEventHandler disconnected;
        public delegate void disconnectionEventHandler();
        
        //incoming message callback and handler definition
        public event serverMessageEventHandler serverMessage;
        public delegate void serverMessageEventHandler(List<MUDTextRun> runs);

        //incoming telnet control sequence callback and handler definition
        public event serverTelnetEventHandler telnetMessage;
        public delegate void serverTelnetEventHandler(string message);

        //a parser/decoder for ANSI control sequences, to give text color and potentially other styling
        ANSIColorParser ansiColorParser = new ANSIColorParser();

        //a parser for Telnet control sequences, which responds to any server messages as required by the Telnet protocol rules
        TelnetParser telnetParser;

        #region initialization, connection

        public MUDServerConnection(string address, int port)
        {
            //try to connect (may throw exceptions, to be handled by caller)
            this.connection.Connect(address, port);

            //if successful
            if (this.connection.Connected)
            {                
                //initialize the telnet parser
                this.telnetParser = new TelnetParser(this.connection);
                
                //start listening for new text
                connection.Client.BeginReceive(this.buffer, 0, this.buffer.Length, SocketFlags.None, new AsyncCallback(this.handleServerMessage), null);
                
                //send a WILL NAWS (negotiate about window size)
                this.telnetParser.sendTelnetBytes((byte)Telnet.WILL, (byte)Telnet.NAWS);
            }
        }

        #endregion        

        #region incoming text handler

        //called when receiving any message
        void handleServerMessage(IAsyncResult result)
        {            
            //get length of data in buffer
            int receivedCount;
            try
            {
                receivedCount = connection.Client.EndReceive(result);
            }
            catch
            {
                //if there was any issue reading the server text, ignore the message (what else can we do?)
                return;
            }
            
            //0 bytes received means the server disconnected
            if (receivedCount == 0)
            {
                this.Disconnect();
                return;
            }

            //list of bytes which aren't telnet sequences
            //ultimately, this will be the original buffer minus any telnet messages from the server
            List<string> telnetMessages;
            List<byte> contentBytes = this.telnetParser.HandleAndRemoveTelnetBytes(this.buffer, receivedCount, out telnetMessages);

            //report any telnet sequences seen to the caller
            App.Current.Dispatcher.BeginInvoke(new Action(delegate
            {
                foreach (string telnetMessage in telnetMessages)
                {
                    //fire the "received a server message" event
                    this.telnetMessage(telnetMessage);
                }
            }));

            //now we've filtered-out and responded accordingly to any telnet data.
            //next, convert the actual MUD content of the message from ASCII to Unicode
            string message = AsciiDecoder.AsciiToUnicode(contentBytes.ToArray(), contentBytes.Count);            
                        
            //run the following on the main thread so that calling code doesn't have to think about threading
            if (this.serverMessage != null)
            {
                App.Current.Dispatcher.BeginInvoke(new Action(delegate
                {
                    //pass the message to the mudTranslator to parse any ANSI control sequences (colors!)
                    List<MUDTextRun> runs = this.ansiColorParser.Translate(message);
                    
                    //fire the "received a server message" event with the runs to be displayed
                    this.serverMessage(runs);
                }));
            }

            //now that we're done with this message, listen for the next message
            connection.Client.BeginReceive(this.buffer, 0, this.buffer.Length, SocketFlags.None, new AsyncCallback(this.handleServerMessage), null);            
        }        

        #endregion
  
        #region outgoing text         

        public void SendText(string text)
        {
            //if not connected, do nothing
            if (!this.connection.Connected) return;

            //add carriage return and line feed
            text = text + "\r\n";

            //convert from Unicode to ASCII
            Encoder encoder = System.Text.Encoding.ASCII.GetEncoder();            
            char[] charArray = text.ToCharArray();
            int count = encoder.GetByteCount(charArray, 0, charArray.Length, true);
            byte[] outputBuffer = new byte[count];
            encoder.GetBytes(charArray, 0, charArray.Length, outputBuffer, 0, true);

            //send to server
            this.connection.Client.Send(outputBuffer);
        }

        #endregion                

        #region disconnect

        internal void Disconnect()
        {
            //if not connected, do nothing
            if (!this.connection.Connected) return;

            //close the connection
            this.connection.Close();

            //initialize a new object
            this.connection = new TcpClient();            
                
            //fire disconnection notification event on main UI thread
            if (this.disconnected != null)
            {
                App.Current.Dispatcher.BeginInvoke(new Action(delegate
                {
                    this.disconnected.Invoke();
                }));
            }
        }

        #endregion        
    }    
}