using System;
using System.Threading;
using Renci.SshNet.Channels;
using Renci.SshNet.Messages.Connection;
using Renci.SshNet.Common;
using System.Globalization;
using System.Net;
namespace Renci.SshNet
{
    /// 
    /// Provides functionality for remote port forwarding
    /// 
    public partial class ForwardedPortRemote : ForwardedPort, IDisposable
    {
        private bool _requestStatus;
        private EventWaitHandle _globalRequestResponse = new AutoResetEvent(false);
        /// 
        /// Gets the bound host.
        /// 
        public IPAddress BoundHostAddress { get; protected set; }
        /// 
        /// Gets the bound host.
        /// 
        public string BoundHost
        {
            get
            {
                return this.BoundHostAddress.ToString();
            }
        }
        /// 
        /// Gets the bound port.
        /// 
        public uint BoundPort { get; protected set; }
        /// 
        /// Gets the forwarded host.
        /// 
        public IPAddress HostAddress { get; protected set; }
        /// 
        /// Gets the forwarded host.
        /// 
        public string Host
        {
            get
            {
                return this.HostAddress.ToString();
            }
        }
        /// 
        /// Gets the forwarded port.
        /// 
        public uint Port { get; protected set; }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The bound host address.
        /// The bound port.
        /// The host address.
        /// The port.
        ///  is null.
        ///  is null.
        ///  is not a valid port.
        ///  is not a valid port.
        public ForwardedPortRemote(IPAddress boundHostAddress, uint boundPort, IPAddress hostAddress, uint port)
        {
            if (boundHostAddress == null)
                throw new ArgumentNullException("boundHostAddress");
            if (hostAddress == null)
                throw new ArgumentNullException("hostAddress");
            if (!boundPort.IsValidPort())
                throw new ArgumentOutOfRangeException("boundPort");
            if (!port.IsValidPort())
                throw new ArgumentOutOfRangeException("port");
            this.BoundHostAddress = boundHostAddress;
            this.BoundPort = boundPort;
            this.HostAddress = hostAddress;
            this.Port = port;
        }
        /// 
        /// Starts remote port forwarding.
        /// 
        public override void Start()
        {
            base.Start();
            //  If port already started don't start it again
            if (this.IsStarted)
                return;
            this.Session.RegisterMessage("SSH_MSG_REQUEST_FAILURE");
            this.Session.RegisterMessage("SSH_MSG_REQUEST_SUCCESS");
            this.Session.RegisterMessage("SSH_MSG_CHANNEL_OPEN");
            this.Session.RequestSuccessReceived += Session_RequestSuccess;
            this.Session.RequestFailureReceived += Session_RequestFailure;
            this.Session.ChannelOpenReceived += Session_ChannelOpening;
            //  Send global request to start direct tcpip
            this.Session.SendMessage(new GlobalRequestMessage(GlobalRequestName.TcpIpForward, true, this.BoundHost, this.BoundPort));
            this.Session.WaitOnHandle(this._globalRequestResponse);
            if (!this._requestStatus)
            {
                //  If request  failed don't handle channel opening for this request
                this.Session.ChannelOpenReceived -= Session_ChannelOpening;
                throw new SshException(string.Format(CultureInfo.CurrentCulture, "Port forwarding for '{0}' port '{1}' failed to start.", this.Host, this.Port));
            }
            this.IsStarted = true;
        }
        /// 
        /// Stops remote port forwarding.
        /// 
        public override void Stop()
        {
            base.Stop();
            //  If port not started you cant stop it
            if (!this.IsStarted)
                return;
            //  Send global request to cancel direct tcpip
            this.Session.SendMessage(new GlobalRequestMessage(GlobalRequestName.CancelTcpIpForward, true, this.BoundHost, this.BoundPort));
            this.Session.WaitOnHandle(this._globalRequestResponse);
            this.Session.RequestSuccessReceived -= Session_RequestSuccess;
            this.Session.RequestFailureReceived -= Session_RequestFailure;
            this.Session.ChannelOpenReceived -= Session_ChannelOpening;
            this.IsStarted = false;
        }
        private void Session_ChannelOpening(object sender, MessageEventArgs e)
        {
            var channelOpenMessage = e.Message;
            var info = channelOpenMessage.Info as ForwardedTcpipChannelInfo;
            if (info != null)
            {
                //  Ensure this is the corresponding request
                if (info.ConnectedAddress == this.BoundHost && info.ConnectedPort == this.BoundPort)
                {
                    this.ExecuteThread(() =>
                    {
                        try
                        {
                            this.RaiseRequestReceived(info.OriginatorAddress, info.OriginatorPort);
                            var channel = this.Session.CreateServerChannel(
                                channelOpenMessage.LocalChannelNumber, channelOpenMessage.InitialWindowSize,
                                channelOpenMessage.MaximumPacketSize);
                            channel.Bind(this.HostAddress, this.Port);
                        }
                        catch (Exception exp)
                        {
                            this.RaiseExceptionEvent(exp);
                        }
                    });
                }
            }
        }
        private void Session_RequestFailure(object sender, EventArgs e)
        {
            this._requestStatus = false;
            this._globalRequestResponse.Set();
        }
        private void Session_RequestSuccess(object sender, MessageEventArgs e)
        {
            this._requestStatus = true;
            if (this.BoundPort == 0)
            {
                this.BoundPort = (e.Message.BoundPort == null) ? 0 : e.Message.BoundPort.Value;
            }
            this._globalRequestResponse.Set();
        }
        partial void ExecuteThread(Action action);
        #region IDisposable Members
        private bool _isDisposed;
        /// 
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
        /// 
        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        /// 
        /// Releases unmanaged and - optionally - managed resources
        /// 
        /// true to release both managed and unmanaged resources; false to release only unmanaged resources.
        protected virtual void Dispose(bool disposing)
        {
            // Check to see if Dispose has already been called.
            if (!this._isDisposed)
            {
                // If disposing equals true, dispose all managed
                // and unmanaged resources.
                if (disposing)
                {
                    // Dispose managed resources.
                    if (this._globalRequestResponse != null)
                    {
                        this._globalRequestResponse.Dispose();
                        this._globalRequestResponse = null;
                    }
                }
                // Note disposing has been done.
                _isDisposed = true;
            }
        }
        /// 
        /// Releases unmanaged resources and performs other cleanup operations before the
        ///  is reclaimed by garbage collection.
        /// 
        ~ForwardedPortRemote()
        {
            // Do not re-create Dispose clean-up code here.
            // Calling Dispose(false) is optimal in terms of
            // readability and maintainability.
            Dispose(false);
        }
        #endregion
    }
}