using System;
using System.Linq;
using System.Text;
using Renci.SshNet.Channels;
using System.IO;
using Renci.SshNet.Common;
using System.Text.RegularExpressions;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace Renci.SshNet
{
    /// 
    /// Provides SCP client functionality.
    /// 
    public partial class ScpClient : BaseClient
    {
        private static readonly Regex _fileInfoRe = new Regex(@"C(?\d{4}) (?\d+) (?.+)");
        private static char[] _byteToChar;
        /// 
        /// Gets or sets the operation timeout.
        /// 
        /// 
        /// The timeout to wait until an operation completes. The default value is negative
        /// one (-1) milliseconds, which indicates an infinite time-out period.
        /// 
        public TimeSpan OperationTimeout { get; set; }
        /// 
        /// Gets or sets the size of the buffer.
        /// 
        /// 
        /// The size of the buffer. The default buffer size is 16384 bytes.
        /// 
        public uint BufferSize { get; set; }
        /// 
        /// Occurs when downloading file.
        /// 
        public event EventHandler Downloading;
        /// 
        /// Occurs when uploading file.
        /// 
        public event EventHandler Uploading;
        #region Constructors
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The connection info.
        ///  is null.
        public ScpClient(ConnectionInfo connectionInfo)
            : this(connectionInfo, false)
        {
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// Connection host.
        /// Connection port.
        /// Authentication username.
        /// Authentication password.
        ///  is null.
        ///  is invalid, or  is null or contains whitespace characters.
        ///  is not within  and .
        [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
        public ScpClient(string host, int port, string username, string password)
            : this(new PasswordConnectionInfo(host, port, username, password), true)
        {
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// Connection host.
        /// Authentication username.
        /// Authentication password.
        ///  is null.
        ///  is invalid, or  is null or contains whitespace characters.
        public ScpClient(string host, string username, string password)
            : this(host, ConnectionInfo.DEFAULT_PORT, username, password)
        {
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// Connection host.
        /// Connection port.
        /// Authentication username.
        /// Authentication private key file(s) .
        ///  is null.
        ///  is invalid, -or-  is null or contains whitespace characters.
        ///  is not within  and .
        [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
        public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
            : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
        {
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// Connection host.
        /// Authentication username.
        /// Authentication private key file(s) .
        ///  is null.
        ///  is invalid, -or-  is null or contains whitespace characters.
        public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles)
            : this(host, ConnectionInfo.DEFAULT_PORT, username, keyFiles)
        {
        }
        /// 
        /// Initializes a new instance of the  class.
        /// 
        /// The connection info.
        /// Specified whether this instance owns the connection info.
        ///  is null.
        /// 
        /// If  is true, then the
        /// connection info will be disposed when this instance is disposed.
        /// 
        private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
            : base(connectionInfo, ownsConnectionInfo)
        {
            this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
            this.BufferSize = 1024 * 16;
            if (_byteToChar == null)
            {
                _byteToChar = new char[128];
                var ch = '\0';
                for (int i = 0; i < 128; i++)
                {
                    _byteToChar[i] = ch++;
                }
            }
        }
        #endregion
        /// 
        /// Uploads the specified stream to the remote host.
        /// 
        /// Stream to upload.
        /// Remote host file name.
        public void Upload(Stream source, string path)
        {
            using (var input = new PipeStream())
            using (var channel = this.Session.CreateChannelSession())
            {
                channel.DataReceived += delegate(object sender, ChannelDataEventArgs e)
                {
                    input.Write(e.Data, 0, e.Data.Length);
                    input.Flush();
                };
                channel.Open();
                int pathEnd = path.LastIndexOfAny(new[] { '\\', '/' });
                if (pathEnd != -1)
                {
                    // split the path from the file
                    string pathOnly = path.Substring(0, pathEnd);
                    string fileOnly = path.Substring(pathEnd + 1);
                    //  Send channel command request
                    channel.SendExecRequest(string.Format("scp -t \"{0}\"", pathOnly));
                    this.CheckReturnCode(input);
                    path = fileOnly;
                }
                this.InternalUpload(channel, input, source, path);
                channel.Close();
            }
        }
        /// 
        /// Downloads the specified file from the remote host to the stream.
        /// 
        /// Remote host file name.
        /// The stream where to download remote file.
        ///  is null or contains whitespace characters.
        ///  is null.
        /// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream.
        public void Download(string filename, Stream destination)
        {
            if (filename.IsNullOrWhiteSpace())
                throw new ArgumentException("filename");
            if (destination == null)
                throw new ArgumentNullException("destination");
            using (var input = new PipeStream())
            using (var channel = this.Session.CreateChannelSession())
            {
                channel.DataReceived += delegate(object sender, ChannelDataEventArgs e)
                {
                    input.Write(e.Data, 0, e.Data.Length);
                    input.Flush();
                };
                channel.Open();
                //  Send channel command request
                channel.SendExecRequest(string.Format("scp -f \"{0}\"", filename));
                this.SendConfirmation(channel); //  Send reply
                var message = ReadString(input);
                var match = _fileInfoRe.Match(message);
                if (match.Success)
                {
                    //  Read file
                    this.SendConfirmation(channel); //  Send reply
                    var mode = match.Result("${mode}");
                    var length = long.Parse(match.Result("${length}"));
                    var fileName = match.Result("${filename}");
                    this.InternalDownload(channel, input, destination, fileName, length);
                }
                else
                {
                    this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
                }
                channel.Close();
            }
        }
        private void InternalSetTimestamp(IChannelSession channel, Stream input, DateTime lastWriteTime, DateTime lastAccessime)
        {
            var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
            var modificationSeconds = (long)(lastWriteTime - zeroTime).TotalSeconds;
            var accessSeconds = (long)(lastAccessime - zeroTime).TotalSeconds;
            this.SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
            this.CheckReturnCode(input);
        }
        private void InternalUpload(IChannelSession channel, Stream input, Stream source, string filename)
        {
            var length = source.Length;
            this.SendData(channel, string.Format("C0644 {0} {1}\n", length, Path.GetFileName(filename)));
            var buffer = new byte[this.BufferSize];
            var read = source.Read(buffer, 0, buffer.Length);
            long totalRead = 0;
            while (read > 0)
            {
                this.SendData(channel, buffer, read);
                totalRead += read;
                this.RaiseUploadingEvent(filename, length, totalRead);
                read = source.Read(buffer, 0, buffer.Length);
            }
            this.SendConfirmation(channel);
            this.CheckReturnCode(input);
        }
        private void InternalDownload(IChannelSession channel, Stream input, Stream output, string filename, long length)
        {
            var buffer = new byte[Math.Min(length, this.BufferSize)];
            var needToRead = length;
            do
            {
                var read = input.Read(buffer, 0, (int)Math.Min(needToRead, this.BufferSize));
                output.Write(buffer, 0, read);
                this.RaiseDownloadingEvent(filename, length, length - needToRead);
                needToRead -= read;
            }
            while (needToRead > 0);
            output.Flush();
            //  Raise one more time when file downloaded
            this.RaiseDownloadingEvent(filename, length, length - needToRead);
            //  Send confirmation byte after last data byte was read
            this.SendConfirmation(channel);
            this.CheckReturnCode(input);
        }
        private void RaiseDownloadingEvent(string filename, long size, long downloaded)
        {
            if (this.Downloading != null)
            {
                this.Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded));
            }
        }
        private void RaiseUploadingEvent(string filename, long size, long uploaded)
        {
            if (this.Uploading != null)
            {
                this.Uploading(this, new ScpUploadEventArgs(filename, size, uploaded));
            }
        }
        private void SendConfirmation(IChannelSession channel)
        {
            this.SendData(channel, new byte[] { 0 });
        }
        private void SendConfirmation(IChannelSession channel, byte errorCode, string message)
        {
            this.SendData(channel, new[] { errorCode });
            this.SendData(channel, string.Format("{0}\n", message));
        }
        /// 
        /// Checks the return code.
        /// 
        /// The output stream.
        private void CheckReturnCode(Stream input)
        {
            var b = ReadByte(input);
            if (b > 0)
            {
                var errorText = ReadString(input);
                throw new ScpException(errorText);
            }
        }
        partial void SendData(IChannelSession channel, string command);
        private void SendData(IChannelSession channel, byte[] buffer, int length)
        {
            if (length == buffer.Length)
            {
                channel.SendData(buffer);
            }
            else
            {
                channel.SendData(buffer.Take(length).ToArray());
            }
        }
        private void SendData(IChannelSession channel, byte[] buffer)
        {
            channel.SendData(buffer);
        }
        private static int ReadByte(Stream stream)
        {
            var b = stream.ReadByte();
            while (b < 0)
            {
                Thread.Sleep(100);
                b = stream.ReadByte();
            }
            return b;
        }
        private static string ReadString(Stream stream)
        {
            var hasError = false;
            var sb = new StringBuilder();
            var b = ReadByte(stream);
            if (b == 1 || b == 2)
            {
                hasError = true;
                b = ReadByte(stream);
            }
            var ch = _byteToChar[b];
            while (ch != '\n')
            {
                sb.Append(ch);
                b = ReadByte(stream);
                ch = _byteToChar[b];
            }
            if (hasError)
                throw new ScpException(sb.ToString());
            return sb.ToString();
        }
    }
}