using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Transport;
namespace Renci.SshNet
{
    public partial class Session
    {
        private const byte Null = 0x00;
        private const byte CarriageReturn = 0x0d;
        private const byte LineFeed = 0x0a;
        private readonly AutoResetEvent _connectEvent = new AutoResetEvent(false);
        private readonly AutoResetEvent _sendEvent = new AutoResetEvent(false);
        private readonly AutoResetEvent _receiveEvent = new AutoResetEvent(false);
        /// 
        /// Gets a value indicating whether the socket is connected.
        /// 
        /// true if the socket is connected; otherwise, false
        partial void IsSocketConnected(ref bool isConnected)
        {
            isConnected = (_socket != null && _socket.Connected);
        }
        /// 
        /// Establishes a socket connection to the specified host and port.
        /// 
        /// The host name of the server to connect to.
        /// The port to connect to.
        /// The connection failed to establish within the configured .
        /// An error occurred trying to establish the connection.
        partial void SocketConnect(string host, int port)
        {
            var timeout = ConnectionInfo.Timeout;
            var ipAddress = host.GetIPAddress();
            var ep = new IPEndPoint(ipAddress, port);
            _socket = new Socket(ep.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
            var args = CreateSocketAsyncEventArgs(_connectEvent);
            if (_socket.ConnectAsync(args))
            {
                if (!_connectEvent.WaitOne(timeout))
                    throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture,
                        "Connection failed to establish within {0:F0} milliseconds.", timeout.TotalMilliseconds));
            }
            if (args.SocketError != SocketError.Success)
                throw new SocketException((int) args.SocketError);
        }
        /// 
        /// Closes the socket.
        /// 
        /// 
        /// This method will wait up to 10 seconds to send any remaining data.
        /// 
        partial void SocketDisconnect()
        {
            _socket.Close(10);
        }
        /// 
        /// Performs a blocking read on the socket until a line is read.
        /// 
        /// The line read from the socket, or null when the remote server has shutdown and all data has been received.
        /// A  that represents the time to wait until a line is read.
        /// The read has timed-out.
        /// An error occurred when trying to access the socket.
        partial void SocketReadLine(ref string response, TimeSpan timeout)
        {
            var encoding = new ASCIIEncoding();
            var buffer = new List();
            var data = new byte[1];
            // read data one byte at a time to find end of line and leave any unhandled information in the buffer
            // to be processed by subsequent invocations
            do
            {
                var args = CreateSocketAsyncEventArgs(_receiveEvent, data, 0, data.Length);
                if (_socket.ReceiveAsync(args))
                {
                    if (!_receiveEvent.WaitOne(timeout))
                        throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture,
                            "Socket read operation has timed out after {0:F0} milliseconds.", timeout.TotalMilliseconds));
                }
                if (args.SocketError != SocketError.Success)
                    throw new SocketException((int) args.SocketError);
                if (args.BytesTransferred == 0)
                    // the remote server shut down the socket
                    break;
                buffer.Add(data[0]);
            }
            while (!(buffer.Count > 0 && (buffer[buffer.Count - 1] == LineFeed || buffer[buffer.Count - 1] == Null)));
            if (buffer.Count == 0)
                response = null;
            else if (buffer.Count == 1 && buffer[buffer.Count - 1] == 0x00)
                // return an empty version string if the buffer consists of only a 0x00 character
                response = string.Empty;
            else if (buffer.Count > 1 && buffer[buffer.Count - 2] == CarriageReturn)
                // strip trailing CRLF
                response = encoding.GetString(buffer.ToArray(), 0, buffer.Count - 2);
            else if (buffer.Count > 1 && buffer[buffer.Count - 1] == LineFeed)
                // strip trailing LF
                response = encoding.GetString(buffer.ToArray(), 0, buffer.Count - 1);
            else
                response = encoding.GetString(buffer.ToArray(), 0, buffer.Count);
        }
        /// 
        /// Performs a blocking read on the socket until  bytes are received.
        /// 
        /// The number of bytes to read.
        /// The buffer to read to.
        /// The socket is closed.
        /// The read has timed-out.
        /// The read failed.
        partial void SocketRead(int length, ref byte[] buffer)
        {
            var timeout = Infinite;
            var totalBytesReceived = 0;  // how many bytes are already received
            do
            {
                var args = CreateSocketAsyncEventArgs(_receiveEvent, buffer, totalBytesReceived,
                    length - totalBytesReceived);
                if (_socket.ReceiveAsync(args))
                {
                    if (!_receiveEvent.WaitOne(timeout))
                        // currently we wait indefinitely, so this exception will never be thrown
                        // but let's leave this here anyway as we may revisit this later
                        throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture,
                            "Socket read operation has timed out after {0:F0} milliseconds.", timeout.TotalMilliseconds));
                }
                switch (args.SocketError)
                {
                    case SocketError.WouldBlock:
                    case SocketError.IOPending:
                    case SocketError.NoBufferSpaceAvailable:
                        // socket buffer is probably full, wait and try again
                        Thread.Sleep(30);
                        break;
                    case SocketError.Success:
                        var bytesReceived = args.BytesTransferred;
                        if (bytesReceived > 0)
                        {
                            totalBytesReceived += bytesReceived;
                            continue;
                        }
                        if (_isDisconnecting)
                            throw new SshConnectionException(
                                "An established connection was aborted by the software in your host machine.",
                                DisconnectReason.ConnectionLost);
                        throw new SshConnectionException("An established connection was aborted by the server.",
                            DisconnectReason.ConnectionLost);
                    default:
                        throw new SocketException((int) args.SocketError);
                }
            } while (totalBytesReceived < length);
        }
        /// 
        /// Writes the specified data to the server.
        /// 
        /// The data to write to the server.
        /// The write has timed-out.
        /// The write failed.
        partial void SocketWrite(byte[] data)
        {
            var timeout = ConnectionInfo.Timeout;
            var totalBytesSent = 0;  // how many bytes are already sent
            var totalBytesToSend = data.Length;
            do
            {
                var args = CreateSocketAsyncEventArgs(_sendEvent, data, 0, totalBytesToSend - totalBytesSent);
                if (_socket.SendAsync(args))
                {
                    if (!_sendEvent.WaitOne(timeout))
                        throw new SshOperationTimeoutException(string.Format(CultureInfo.InvariantCulture,
                            "Socket write operation has timed out after {0:F0} milliseconds.", timeout.TotalMilliseconds));
                }
                switch (args.SocketError)
                {
                    case SocketError.WouldBlock:
                    case SocketError.IOPending:
                    case SocketError.NoBufferSpaceAvailable:
                        // socket buffer is probably full, wait and try again
                        Thread.Sleep(30);
                        break;
                    case SocketError.Success:
                        totalBytesSent += args.BytesTransferred;
                        break;
                    default:
                        throw new SocketException((int) args.SocketError);
}
                } while (totalBytesSent < totalBytesToSend);
        }
        partial void ExecuteThread(Action action)
        {
            ThreadPool.QueueUserWorkItem(o => action());
        }
        partial void InternalRegisterMessage(string messageName)
        {
            lock (_messagesMetadata)
            {
                foreach (var item in from m in _messagesMetadata where m.Name == messageName select m)
                {
                    item.Enabled = true;
                    item.Activated = true;
                }
            }
        }
        partial void InternalUnRegisterMessage(string messageName)
        {
            lock (_messagesMetadata)
            {
                foreach (var item in from m in _messagesMetadata where m.Name == messageName select m)
                {
                    item.Enabled = false;
                    item.Activated = false;
                }
            }
        }
        private SocketAsyncEventArgs CreateSocketAsyncEventArgs(EventWaitHandle waitHandle)
        {
            var args = new SocketAsyncEventArgs();
            args.UserToken = _socket;
            args.RemoteEndPoint = _socket.RemoteEndPoint;
            args.Completed += (sender, eventArgs) => waitHandle.Set();
            return args;
        }
        private SocketAsyncEventArgs CreateSocketAsyncEventArgs(EventWaitHandle waitHandle, byte[] data, int offset, int count)
        {
            var args = CreateSocketAsyncEventArgs(waitHandle);
            args.SetBuffer(data, offset, count);
            return args;
        }
    }
}