using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using Renci.SshNet.Channels;
using Renci.SshNet.Common;
using Renci.SshNet.Messages;
using Renci.SshNet.Messages.Connection;
using Renci.SshNet.Messages.Transport;
using System.Globalization;
namespace Renci.SshNet
{
    /// 
    /// Represents SSH command that can be executed.
    /// 
    public partial class SshCommand : IDisposable
    {
        private Encoding _encoding;
        private Session _session;
        private ChannelSession _channel;
        private CommandAsyncResult _asyncResult;
        private AsyncCallback _callback;
        private EventWaitHandle _sessionErrorOccuredWaitHandle = new AutoResetEvent(false);
        private Exception _exception;
        private bool _hasError;
        private object _endExecuteLock = new object();
        /// 
        /// Gets the command text.
        /// 
        public string CommandText { get; private set; }
        /// 
        /// Gets or sets the command timeout.
        /// 
        /// 
        /// The command timeout.
        /// 
        public TimeSpan CommandTimeout { get; set; }
        /// 
        /// Gets the command exit status.
        /// 
        public int ExitStatus { get; private set; }
        /// 
        /// Gets the output stream.
        /// 
        public Stream OutputStream { get; private set; }
        /// 
        /// Gets the extended output stream.
        /// 
        public Stream ExtendedOutputStream { get; private set; }
        private StringBuilder _result;
        /// 
        /// Gets the command execution result.
        /// 
        public string Result
        {
            get
            {
                if (this._result == null)
                {
                    this._result = new StringBuilder();
                }
                if (this.OutputStream != null && this.OutputStream.Length > 0)
                {
                    using (var sr = new StreamReader(this.OutputStream, this._encoding))
                    {
                        this._result.Append(sr.ReadToEnd());
                    }
                }
                return this._result.ToString();
            }
        }
        private StringBuilder _error;
        /// 
        /// Gets the command execution error.
        /// 
        public string Error
        {
            get
            {
                if (this._hasError)
                {
                    if (this._error == null)
                    {
                        this._error = new StringBuilder();
                    }
                    if (this.ExtendedOutputStream != null && this.ExtendedOutputStream.Length > 0)
                    {
                        using (var sr = new StreamReader(this.ExtendedOutputStream, this._encoding))
                        {
                            this._error.Append(sr.ReadToEnd());
                        }
                    }
                    return this._error.ToString();
                }
                else
                    return string.Empty;
            }
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The session.
        /// The command text.
        /// The encoding.
        internal SshCommand(Session session, string commandText, Encoding encoding)
        {
            if (session == null)
                throw new ArgumentNullException("session");
            this._encoding = encoding;
            this._session = session;
            this.CommandText = commandText;
            this.CommandTimeout = new TimeSpan(0, 0, 0, 0, -1);
            this._session.Disconnected += Session_Disconnected;
            this._session.ErrorOccured += Session_ErrorOccured;
        }
        /// 
        /// Begins an asynchronous command execution.
        /// 
        /// An optional asynchronous callback, to be called when the command execution is complete.
        /// A user-provided object that distinguishes this particular asynchronous read request from other requests.
        /// An  that represents the asynchronous command execution, which could still be pending.
        /// Client is not connected.
        /// Operation has timed out.
        public IAsyncResult BeginExecute(AsyncCallback callback, object state)
        {
            //  Prevent from executing BeginExecute before calling EndExecute
            if (this._asyncResult != null)
            {
                throw new InvalidOperationException("Asynchronous operation is already in progress.");
            }
            //  Create new AsyncResult object
            this._asyncResult = new CommandAsyncResult(this)
            {
                AsyncWaitHandle = new EventWaitHandle(false, EventResetMode.ManualReset),
                IsCompleted = false,
                AsyncState = state,
            };
            //  When command re-executed again, create a new channel
            if (this._channel != null)
            {
                throw new SshException("Invalid operation.");
            }
            this.CreateChannel();
            if (string.IsNullOrEmpty(this.CommandText))
                throw new ArgumentException("CommandText property is empty.");
            this._callback = callback;
            this._channel.Open();
            //  Send channel command request
            this._channel.SendExecRequest(this.CommandText);
            return _asyncResult;
        }
        /// 
        /// Begins an asynchronous command execution.
        /// 
        /// The command text.
        /// An optional asynchronous callback, to be called when the command execution is complete.
        /// A user-provided object that distinguishes this particular asynchronous read request from other requests.
        /// An  that represents the asynchronous command execution, which could still be pending.
        /// Client is not connected.
        /// Operation has timed out.
        public IAsyncResult BeginExecute(string commandText, AsyncCallback callback, object state)
        {
            this.CommandText = commandText;
            return BeginExecute(callback, state);
        }
        /// 
        /// Waits for the pending asynchronous command execution to complete.
        /// 
        /// The reference to the pending asynchronous request to finish.
        /// 
        public string EndExecute(IAsyncResult asyncResult)
        {
            if (this._asyncResult == asyncResult && this._asyncResult != null)
            {
                lock (this._endExecuteLock)
                {
                    if (this._asyncResult != null)
                    {
                        //  Make sure that operation completed if not wait for it to finish
                        this.WaitHandle(this._asyncResult.AsyncWaitHandle);
                        this._channel.Close();
                        this._channel = null;
                        this._asyncResult = null;
                        return this.Result;
                    }
                }
            }
            throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
        }
        /// 
        /// Executes command specified by  property.
        /// 
        /// Command execution result
        /// Client is not connected.
        /// Operation has timed out.
        public string Execute()
        {
            return this.EndExecute(this.BeginExecute(null, null));
        }
        /// 
        /// Cancels command execution in asynchronous scenarios. CURRENTLY NOT IMPLEMENTED.
        /// 
        //public void Cancel()
        //{
        //    if (this._channel != null && this._channel.IsOpen)
        //    {
        //        //this._channel.SendData(Encoding.ASCII.GetBytes("~."));
        //        this._channel.SendExecRequest("\0x03");
        //        //this._channel.SendSignalRequest("ABRT");
        //        //this._channel.SendSignalRequest("ALRM");
        //        //this._channel.SendSignalRequest("FPE");
        //        //this._channel.SendSignalRequest("HUP");
        //        //this._channel.SendSignalRequest("ILL");
        //        //this._channel.SendSignalRequest("INT");
        //        //this._channel.SendSignalRequest("PIPE");
        //        //this._channel.SendSignalRequest("QUIT");
        //        //this._channel.SendSignalRequest("SEGV");
        //        //this._channel.SendSignalRequest("TERM");
        //        //this._channel.SendSignalRequest("SEGV");
        //        //this._channel.SendSignalRequest("USR1");
        //        //this._channel.SendSignalRequest("USR2");
        //    }
        //}
        /// 
        /// Executes the specified command text.
        /// 
        /// The command text.
        /// Command execution result
        /// Client is not connected.
        /// Operation has timed out.
        public string Execute(string commandText)
        {
            this.CommandText = commandText;
            return this.Execute();
        }
        private void CreateChannel()
        {
            this._channel = this._session.CreateChannel();
            this._channel.DataReceived += Channel_DataReceived;
            this._channel.ExtendedDataReceived += Channel_ExtendedDataReceived;
            this._channel.RequestReceived += Channel_RequestReceived;
            this._channel.Closed += Channel_Closed;
            //  Dispose of streams if already exists
            if (this.OutputStream != null)
            {
                this.OutputStream.Dispose();
                this.OutputStream = null;
            }
            if (this.ExtendedOutputStream != null)
            {
                this.ExtendedOutputStream.Dispose();
                this.ExtendedOutputStream = null;
            }
            //  Initialize output streams and StringBuilders
            this.OutputStream = new PipeStream();
            this.ExtendedOutputStream = new PipeStream();
            this._result = null;
            this._error = null;
        }
        private void Session_Disconnected(object sender, EventArgs e)
        {
            this._exception = new SshConnectionException("An established connection was aborted by the software in your host machine.", DisconnectReason.ConnectionLost);
            this._sessionErrorOccuredWaitHandle.Set();
        }
        private void Session_ErrorOccured(object sender, ExceptionEventArgs e)
        {
            this._exception = e.Exception;
            this._sessionErrorOccuredWaitHandle.Set();
        }
        private void Channel_Closed(object sender, Common.ChannelEventArgs e)
        {
            if (this.OutputStream != null)
            {
                this.OutputStream.Flush();
            }
            if (this.ExtendedOutputStream != null)
            {
                this.ExtendedOutputStream.Flush();
            }
            this._asyncResult.IsCompleted = true;
            if (this._callback != null)
            {
                //  Execute callback on different thread                
                this.ExecuteThread(() => { this._callback(this._asyncResult); });
            }
            ((EventWaitHandle)this._asyncResult.AsyncWaitHandle).Set();
        }
        private void Channel_RequestReceived(object sender, Common.ChannelRequestEventArgs e)
        {
            Message replyMessage = new ChannelFailureMessage(this._channel.LocalChannelNumber);
            if (e.Info is ExitStatusRequestInfo)
            {
                ExitStatusRequestInfo exitStatusInfo = e.Info as ExitStatusRequestInfo;
                this.ExitStatus = (int)exitStatusInfo.ExitStatus;
                replyMessage = new ChannelSuccessMessage(this._channel.LocalChannelNumber);
            }
            if (e.Info.WantReply)
            {
                this._session.SendMessage(replyMessage);
            }
        }
        private void Channel_ExtendedDataReceived(object sender, Common.ChannelDataEventArgs e)
        {
            if (this.ExtendedOutputStream != null)
            {
                this.ExtendedOutputStream.Write(e.Data, 0, e.Data.Length);
                this.ExtendedOutputStream.Flush();
            }
            if (e.DataTypeCode == 1)
            {
                this._hasError = true;
            }
        }
        private void Channel_DataReceived(object sender, Common.ChannelDataEventArgs e)
        {
            if (this.OutputStream != null)
            {
                this.OutputStream.Write(e.Data, 0, e.Data.Length);
                //this._outputSteamWriter.Write(this._encoding.GetString(e.Data, 0, e.Data.Length));
                this.OutputStream.Flush();
            }
            if (this._asyncResult != null)
            {
                lock (this._asyncResult)
                {
                    this._asyncResult.BytesReceived += e.Data.Length;
                }
            }
        }
        private void WaitHandle(WaitHandle waitHandle)
        {
            var waitHandles = new WaitHandle[]
                {
                    this._sessionErrorOccuredWaitHandle,
                    waitHandle,
                };
            var index = EventWaitHandle.WaitAny(waitHandles, this.CommandTimeout);
            if (index < 1)
            {
                throw this._exception;
            }
            else if (index > 1)
            {
                //  throw time out error
                throw new SshOperationTimeoutException(string.Format(CultureInfo.CurrentCulture, "Command '{0}' has timed out.", this.CommandText));
            }
        }
        
        partial void ExecuteThread(Action action);
        #region IDisposable Members
        private bool _isDisposed = false;
        /// 
        /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged ResourceMessages.
        /// 
        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 ResourceMessages.
        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 ResourceMessages.
                if (disposing)
                {
                    this._session.Disconnected -= Session_Disconnected;
                    this._session.ErrorOccured -= Session_ErrorOccured;
                    // Dispose managed ResourceMessages.
                    if (this.OutputStream != null)
                    {
                        this.OutputStream.Dispose();
                        this.OutputStream = null;
                    }
                    // Dispose managed ResourceMessages.
                    if (this.ExtendedOutputStream != null)
                    {
                        this.ExtendedOutputStream.Dispose();
                        this.ExtendedOutputStream = null;
                    }
                    // Dispose managed ResourceMessages.
                    if (this._sessionErrorOccuredWaitHandle != null)
                    {
                        this._sessionErrorOccuredWaitHandle.Dispose();
                        this._sessionErrorOccuredWaitHandle = null;
                    }
                    // Dispose managed ResourceMessages.
                    if (this._channel != null)
                    {
                        this._channel.DataReceived -= Channel_DataReceived;
                        this._channel.ExtendedDataReceived -= Channel_ExtendedDataReceived;
                        this._channel.RequestReceived -= Channel_RequestReceived;
                        this._channel.Closed -= Channel_Closed;
                        this._channel.Dispose();
                        this._channel = null;
                    }
                }
                // Note disposing has been done.
                this._isDisposed = true;
            }
        }
        /// 
        /// Releases unmanaged resources and performs other cleanup operations before the
        ///  is reclaimed by garbage collection.
        /// 
        ~SshCommand()
        {
            // Do not re-create Dispose clean-up code here.
            // Calling Dispose(false) is optimal in terms of
            // readability and maintainability.
            Dispose(false);
        }
        #endregion
    }
}