using System;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Renci.SshNet.Common;
using Renci.SshNet.Messages.Connection;
namespace Renci.SshNet.Channels
{
    /// 
    /// Implements "direct-tcpip" SSH channel.
    /// 
    internal partial class ChannelDirectTcpip : ClientChannel, IChannelDirectTcpip
    {
        private readonly object _socketShutdownAndCloseLock = new object();
        private EventWaitHandle _channelEof = new AutoResetEvent(false);
        private EventWaitHandle _channelOpen = new AutoResetEvent(false);
        private EventWaitHandle _channelData = new AutoResetEvent(false);
        /// 
        /// An  that is signaled when the blocking receive is cancelled because the
        /// forwarded port is closing.
        /// 
        private EventWaitHandle _channelInterrupted = new ManualResetEvent(false);
        private IForwardedPort _forwardedPort;
        private Socket _socket;
        /// 
        /// Holds a value indicating whether the SSH_MSG_CHANNEL_EOF has been sent to the server.
        /// 
        /// 
        /// 0 when the SSH_MSG_CHANNEL_EOF message has not been sent to the server, and
        /// 1 when this message was already sent.
        /// 
        private int _sentEof;
        /// 
        /// Gets the type of the channel.
        /// 
        /// 
        /// The type of the channel.
        /// 
        public override ChannelTypes ChannelType
        {
            get { return ChannelTypes.DirectTcpip; }
        }
        public void Open(string remoteHost, uint port, IForwardedPort forwardedPort, Socket socket)
        {
            if (IsOpen)
                throw new SshException("Channel is already open.");
            if (!IsConnected)
                throw new SshException("Session is not connected.");
            _socket = socket;
            _forwardedPort = forwardedPort;
            _forwardedPort.Closing += ForwardedPort_Closing;
            _sentEof = 0;
            var ep = socket.RemoteEndPoint as IPEndPoint;
            // open channel
            SendMessage(new ChannelOpenMessage(LocalChannelNumber, LocalWindowSize, LocalPacketSize,
                new DirectTcpipChannelInfo(remoteHost, port, ep.Address.ToString(), (uint) ep.Port)));
            //  Wait for channel to open
            WaitOnHandle(_channelOpen);
        }
        /// 
        /// Occurs as the forwarded port is being stopped.
        /// 
        private void ForwardedPort_Closing(object sender, EventArgs eventArgs)
        {
            CloseSocket();
        }
        /// 
        /// Binds channel to remote host.
        /// 
        public void Bind()
        {
            //  Cannot bind if channel is not open
            if (!IsOpen)
                return;
            var buffer = new byte[RemotePacketSize];
            while (_socket != null && _socket.Connected)
            {
                try
                {
                    var read = 0;
                    InternalSocketReceive(buffer, ref read);
                    if (read > 0)
                    {
                        SendMessage(new ChannelDataMessage(RemoteChannelNumber, buffer.Take(read).ToArray()));
                    }
                    else
                    {
                        // client quit sending (but the server may still send data or an EOF)
                        if (Interlocked.CompareExchange(ref _sentEof, 1, 0) == 0)
                        {
                            // inform server that we won't be sending anything anymore if we
                            // haven't already sent a SSH_MSG_CHANNEL_EOF message
                            //
                            // note that we'll still wait for a SSH_MSG_CHANNEL_EOF or
                            // SSH_MSG_CHANNEL_CLOSE message once we've broken the receive
                            // loop
                            SendEof();
                        }
                        break;
                    }
                }
                catch (SocketException exp)
                {
                    switch (exp.SocketErrorCode)
                    {
                        case SocketError.WouldBlock:
                        case SocketError.IOPending:
                        case SocketError.NoBufferSpaceAvailable:
                            // socket buffer is probably empty, wait and try again
                            Thread.Sleep(30);
                            break;
                        case SocketError.ConnectionAborted:
                        case SocketError.ConnectionReset:
                            // connection was closed after receiving SSH_MSG_CHANNEL_CLOSE message
                            // in which case the _channelEof waithandle is also set
                            break;
                        case SocketError.Interrupted:
                            // connection was interrupted as part of closing the forwarded port
                            _channelInterrupted.Set();
                            break;
                        default:
                            throw; // throw any other error
                    }
                }
            }
            WaitHandle.WaitAny(new WaitHandle[] {_channelEof, _channelInterrupted});
        }
        /// 
        /// Closes the socket, hereby interrupting the blocking receive in .
        /// 
        private void CloseSocket()
        {
            if (_socket == null || !_socket.Connected)
                return;
            lock (_socketShutdownAndCloseLock)
            {
                if (_socket == null || !_socket.Connected)
                    return;
                _socket.Shutdown(SocketShutdown.Both);
                _socket.Close();
            }
        }
        private void ShutdownSocket(SocketShutdown how)
        {
            if (_socket == null || !_socket.Connected)
                return;
            lock (_socketShutdownAndCloseLock)
            {
                if (_socket == null || !_socket.Connected)
                    return;
                _socket.Shutdown(how);
            }
        }
        /// 
        /// Closes the channel, optionally waiting for the SSH_MSG_CHANNEL_CLOSE message to
        /// be received from the server.
        /// 
        /// true to wait for the SSH_MSG_CHANNEL_CLOSE message to be received from the server; otherwise, false.
        protected override void Close(bool wait)
        {
            if (_forwardedPort != null)
            {
                _forwardedPort.Closing -= ForwardedPort_Closing;
                _forwardedPort = null;
            }
            // close the socket, hereby interrupting the blocking receive in Bind()
            CloseSocket();
            //  send EOF message first when channel needs to be closed
            if (IsOpen && Interlocked.CompareExchange(ref _sentEof, 1, 0) == 0)
            {
                SendEof();
            }
            base.Close(wait);
        }
        /// 
        /// Called when channel data is received.
        /// 
        /// The data.
        protected override void OnData(byte[] data)
        {
            base.OnData(data);
            if (_socket != null && _socket.Connected)
                InternalSocketSend(data);
        }
        /// 
        /// Called when channel is opened by the server.
        /// 
        /// The remote channel number.
        /// Initial size of the window.
        /// Maximum size of the packet.
        protected override void OnOpenConfirmation(uint remoteChannelNumber, uint initialWindowSize, uint maximumPacketSize)
        {
            base.OnOpenConfirmation(remoteChannelNumber, initialWindowSize, maximumPacketSize);
            _channelOpen.Set();
        }
        protected override void OnOpenFailure(uint reasonCode, string description, string language)
        {
            base.OnOpenFailure(reasonCode, description, language);
            _channelOpen.Set();
        }
        /// 
        /// Called when channel has no more data to receive.
        /// 
        protected override void OnEof()
        {
            base.OnEof();
            // the channel will send no more data, so signal to the client that
            // we won't be sending anything anymore
            ShutdownSocket(SocketShutdown.Send);
            var channelEof = _channelEof;
            if (channelEof != null)
                channelEof.Set();
        }
        protected override void OnClose()
        {
            base.OnClose();
            // the channel will send no more data, so signal to the client that
            // we won't be sending anything anymore
            //
            // we need to do this here in case the server sends the SSH_MSG_CHANNEL_CLOSE
            // message without first sending SSH_MSG_CHANNEL_EOF
            ShutdownSocket(SocketShutdown.Send);
            var channelEof = _channelEof;
            if (channelEof != null)
                channelEof.Set();
        }
        /// 
        /// Called whenever an unhandled  occurs in  causing
        /// the message loop to be interrupted, or when an exception occurred processing a channel message.
        /// 
        protected override void OnErrorOccured(Exception exp)
        {
            base.OnErrorOccured(exp);
            // close the socket, hereby interrupting the blocking receive in Bind()
            CloseSocket();
            //  if error occured, no more data can be received
            var channelEof = _channelEof;
            if (channelEof != null)
                channelEof.Set();
        }
        /// 
        /// Called when the server wants to terminate the connection immmediately.
        /// 
        /// 
        /// The sender MUST NOT send or receive any data after this message, and
        /// the recipient MUST NOT accept any data after receiving this message.
        /// 
        protected override void OnDisconnected()
        {
            base.OnDisconnected();
            // close the socket, hereby interrupting the blocking receive in Bind()
            CloseSocket();
            //  If disconnected, no more data can be received
            var channelEof = _channelEof;
            if (channelEof != null)
                channelEof.Set();
        }
        partial void InternalSocketReceive(byte[] buffer, ref int read);
        partial void InternalSocketSend(byte[] data);
        protected override void Dispose(bool disposing)
        {
            // make sure we've unsubscribed from all session events before we starting disposing
            base.Dispose(disposing);
            if (_forwardedPort != null)
            {
                _forwardedPort.Closing -= ForwardedPort_Closing;
                _forwardedPort = null;
            }
            if (_socket != null)
            {
                _socket.Dispose();
                _socket = null;
            }
            if (_channelEof != null)
            {
                _channelEof.Dispose();
                _channelEof = null;
            }
            if (_channelOpen != null)
            {
                _channelOpen.Dispose();
                _channelOpen = null;
            }
            if (_channelData != null)
            {
                _channelData.Dispose();
                _channelData = null;
            }
            if (_channelInterrupted != null)
            {
                _channelInterrupted.Dispose();
                _channelInterrupted = null;
            }
        }
    }
}