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

namespace MUDClientEssentials
{
    enum Telnet : byte
    {
        //escape
        InterpretAsCommand = 255,

        //commands
        SubnegotiationEnd = 240,
        NoOperation = 241,
        DataMark = 242,
        Break = 243,
        InterruptProcess = 244,
        AbortOutput = 245,
        AreYouThere = 246,
        EraseCharacter = 247,
        EraseLine = 248,
        GoAhead = 249,
        SubnegotiationBegin = 250,

        //negotiation
        WILL = 251,
        WONT = 252,
        DO = 253,
        DONT = 254,

        //options (common)
        SuppressGoAhead = 3,
        Status = 5,
        Echo = 1,
        TimingMark = 6,
        TerminalType = 24,
        TerminalSpeed = 32,
        RemoteFlowControl = 33,
        LineMode = 34,
        EnvironmentVariables = 36,
        NAWS = 31,

        //options (MUD-specific)
        MSDP = 69,
        MXP = 91,
        MCCP1 = 85,
        MCCP2 = 86,
        MSP = 90
    };
    
    class TelnetParser
    {
        private System.Net.Sockets.TcpClient tcpClient;

        public TelnetParser(System.Net.Sockets.TcpClient tcpClient)
        {            
            this.tcpClient = tcpClient;
        }        

        public List<byte> HandleAndRemoveTelnetBytes(byte[] buffer, int receivedCount, out List<string> telnetMessages)
        {            
            //list to hold a report of any telnet control sequences received or sent
            telnetMessages = new List<string>();
            
            //list to hold any bytes which aren't telnet bytes (which will be most of the bytes)
            List<byte> contentBytes = new List<byte>();

            //we'll scan for telnet control sequences.  anything NOT a telnet control sequence will be added to the contentBytes list for later processing.
            int currentIndex = 0;
            while (currentIndex < receivedCount)
            {
                //search for an IAC, which may signal the beginning of a telnet message
                while (currentIndex < receivedCount && buffer[currentIndex] != (byte)Telnet.InterpretAsCommand)
                {
                    contentBytes.Add(buffer[currentIndex]);
                    currentIndex++;
                }

                //if at the end of the data, stop.  otherwise we've encountered an IAC and there should be at least one more byte here
                if (++currentIndex == receivedCount) break;

                //read the next byte
                byte secondByte = buffer[currentIndex];

                //if another IAC, then this was just sequence IAC IAC, which is the escape sequence to represent byte value 255 (=IAC) in the content stream
                if (secondByte == (byte)Telnet.InterpretAsCommand)
                {
                    //write byte value 255 to the content stream and move on
                    contentBytes.Add(secondByte);
                }

                //otherwise we have a "real" telnet sequence, where the second byte is a command or negotiation
                else
                {
                    //start building a string representation of this message, to be reported to the caller
                    //caller might want to show this info to the user always, or optionally for debugging purposes
                    StringBuilder stringVersionOfMessage = new StringBuilder();

                    //also build a string version of the response (if any)
                    StringBuilder stringVersionOfResponse = new StringBuilder();

                    //DO
                    if (secondByte == (byte)Telnet.DO)
                    {
                        stringVersionOfMessage.Append("DO ");

                        //what are we being told to do?
                        currentIndex++;
                        if (currentIndex == receivedCount) break;
                        byte thirdByte = buffer[currentIndex];

                        stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte));

                        //if NAWS (negotiate about window size)
                        if (thirdByte == (byte)Telnet.NAWS)
                        {
                            //on connection, we offered to negotiate about window size.  so this is a "go ahead and negotiate" response.
                            //so then, send information about client window size per the NAWS protocol
                            //we're lieing to server by telling it a ridiculously large size, so that it won't do line breaking or paging for us (annoying!)
                            stringVersionOfResponse.Append(this.sendTelnetBytes(
                                (byte)Telnet.SubnegotiationBegin, (byte)31,
                                254, 254, 254, 254,
                                (byte)Telnet.InterpretAsCommand, (byte)Telnet.SubnegotiationEnd));
                        }

                        //everything else the server might ask us to do is unsupported by us
                        else
                        {
                            stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte));

                            //sorry, i won't do whatever "that thing you said to do" was
                            stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.InterpretAsCommand, (byte)Telnet.WONT, thirdByte));
                        }
                    }

                    //DONT
                    else if (secondByte == (byte)Telnet.DONT)
                    {
                        stringVersionOfMessage.Append("DONT ");

                        currentIndex++;
                        if (currentIndex == receivedCount) break;
                        byte thirdByte = buffer[currentIndex];

                        stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte));

                        //whatever you want me to stop doing, that's no problem because i wasn't going to do it anyway                    
                        stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.WONT, thirdByte));
                    }

                    //WILL
                    else if (secondByte == (byte)Telnet.WILL)
                    {
                        stringVersionOfMessage.Append("WILL ");

                        //find out what the server is willing to do
                        currentIndex++;
                        if (currentIndex == receivedCount) break;
                        byte thirdByte = buffer[currentIndex];
                        stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte));

                        //anything the server offers to do for us, we'll tell it not to because we don't know what it is
                        stringVersionOfResponse.Append((this.sendTelnetBytes((byte)Telnet.DONT, thirdByte)));
                    }

                    //WONT
                    else if (secondByte == (byte)Telnet.WONT)
                    {
                        stringVersionOfMessage.Append("WONT ");

                        //find out what the server is NOT willing to do
                        currentIndex++;
                        if (currentIndex == receivedCount) break;
                        byte thirdByte = buffer[currentIndex];

                        stringVersionOfMessage.Append(interpretByteAsTelnet(thirdByte));

                        //because we haven't asked the server to DO anything, should not expect to receive any WONT
                        //however if we do receive a WONT, respond with a DONT to confirm that the server can go ahead and NOT do that thing it doesn't want to do
                        stringVersionOfResponse.Append(this.sendTelnetBytes((byte)Telnet.DONT, thirdByte));
                    }

                    //subnegotiations
                    else if (secondByte == (byte)Telnet.SubnegotiationBegin)
                    {
                        stringVersionOfMessage.Append("SB ");
                        List<byte> subnegotiationBytes = new List<byte>();

                        //read until an IAC followed by an SE
                        while (currentIndex < receivedCount - 1 &&
                            !(buffer[currentIndex] == (byte)Telnet.InterpretAsCommand && buffer[currentIndex] == (byte)Telnet.SubnegotiationEnd))
                        {
                            subnegotiationBytes.Add(buffer[currentIndex]);
                            currentIndex++;
                        }

                        byte[] subnegotiationBytesArray = subnegotiationBytes.ToArray();

                        //append the content of the subnegotiation to the incoming message report string
                        stringVersionOfMessage.Append(AsciiDecoder.AsciiToUnicode(subnegotiationBytesArray, subnegotiationBytes.Count));

                        //append the subnegotiation end
                        stringVersionOfMessage.Append(" SE");
                    }

                    //any other telnet message
                    else
                    {
                        //try to convert it to a known message via the enum defined above
                        stringVersionOfMessage.Append(interpretByteAsTelnet(secondByte));
                    }

                    //report the control sequence we found, if any
                    string messageToReport = stringVersionOfMessage.ToString();
                    if (!string.IsNullOrEmpty(messageToReport))
                    {
                        telnetMessages.Add("RECV: " + messageToReport.ToString());                                               
                    }

                    //report the response message sent, if any
                    string responseToReport = stringVersionOfResponse.ToString();
                    if (!string.IsNullOrEmpty(responseToReport))
                    {
                        telnetMessages.Add("SEND: " + stringVersionOfResponse.ToString());                                               
                    }
                }
                //move up to the next byte in the data
                currentIndex++;
            }

            return contentBytes;
        }

        #region "friendly" text for telnet sequences

        private string interpretByteAsTelnet(byte thisByte)
        {
            //try to convert the byte value to a string representation based on the Telnet enumeration
            string friendlyName = Enum.GetName(typeof(Telnet), thisByte);

            //if failed, just show the byte's numerical value in brackets, like [254]
            if (string.IsNullOrEmpty(friendlyName))
            {
                friendlyName = '[' + thisByte.ToString() + ']';
            }

            return friendlyName;
        }

        #endregion
        
        public string sendTelnetBytes(params byte[] bytes)
        {
            //if not connected, do nothing
            if (!this.tcpClient.Connected) return "";

            //send IAC
            this.tcpClient.Client.Send(new byte[] { (byte)Telnet.InterpretAsCommand });

            //send the specified bytes
            this.tcpClient.Client.Send(bytes);

            //start building a string to report to the caller
            StringBuilder reportMessage = new StringBuilder();

            for (int i = 0; i < bytes.Length; i++)
            {
                //convert the byte value to something readable, and append it to the report string
                reportMessage.Append(this.interpretByteAsTelnet(bytes[i]) + " ");
            }

            return reportMessage.ToString();
        }
    }
}