using System;
using Renci.SshNet.Channels;
using System.IO;
using Renci.SshNet.Common;
using System.Text.RegularExpressions;
namespace Renci.SshNet
{
    /// 
    /// Provides SCP client functionality.
    /// 
    public partial class ScpClient
    {
        private static readonly Regex _directoryInfoRe = new Regex(@"D(?\d{4}) (?\d+) (?.+)");
        private static readonly Regex _timestampRe = new Regex(@"T(?\d+) 0 (?\d+) 0");
        /// 
        /// Uploads the specified file to the remote host.
        /// 
        /// The file system info.
        /// The path.
        ///  is null.
        ///  is null or empty.
        public void Upload(FileInfo fileInfo, string path)
        {
            if (fileInfo == null)
                throw new ArgumentNullException("fileInfo");
            if (string.IsNullOrEmpty(path))
                throw new ArgumentException("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();
                //  Send channel command request
                channel.SendExecRequest(string.Format("scp -t \"{0}\"", path));
                this.CheckReturnCode(input);
                this.InternalUpload(channel, input, fileInfo, fileInfo.Name);
                channel.Close();
            }
        }
        /// 
        /// Uploads the specified directory to the remote host.
        /// 
        /// The directory info.
        /// The path.
        /// fileSystemInfo
        ///  is null or empty.
        public void Upload(DirectoryInfo directoryInfo, string path)
        {
            if (directoryInfo == null)
                throw new ArgumentNullException("directoryInfo");
            if (string.IsNullOrEmpty(path))
                throw new ArgumentException("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();
                //  Send channel command request
                channel.SendExecRequest(string.Format("scp -rt \"{0}\"", path));
                this.CheckReturnCode(input);
                this.InternalSetTimestamp(channel, input, directoryInfo.LastWriteTimeUtc, directoryInfo.LastAccessTimeUtc);
                this.SendData(channel, string.Format("D0755 0 {0}\n", Path.GetFileName(path)));
                this.CheckReturnCode(input);
                this.InternalUpload(channel, input, directoryInfo);
                this.SendData(channel, "E\n");
                this.CheckReturnCode(input);
                channel.Close();
            }
        }
        /// 
        /// Downloads the specified file from the remote host to local file.
        /// 
        /// Remote host file name.
        /// Local file information.
        ///  is null.
        ///  is null or empty.
        public void Download(string filename, FileInfo fileInfo)
        {
            if (string.IsNullOrEmpty(filename))
                throw new ArgumentException("filename");
            if (fileInfo == null)
                throw new ArgumentNullException("fileInfo");
            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 -pf \"{0}\"", filename));
                this.SendConfirmation(channel); //  Send reply
                this.InternalDownload(channel, input, fileInfo);
                channel.Close();
            }
        }
        /// 
        /// Downloads the specified directory from the remote host to local directory.
        /// 
        /// Remote host directory name.
        /// Local directory information.
        ///  is null or empty.
        ///  is null.
        public void Download(string directoryName, DirectoryInfo directoryInfo)
        {
            if (string.IsNullOrEmpty(directoryName))
                throw new ArgumentException("directoryName");
            if (directoryInfo == null)
                throw new ArgumentNullException("directoryInfo");
            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 -prf \"{0}\"", directoryName));
                this.SendConfirmation(channel); //  Send reply
                this.InternalDownload(channel, input, directoryInfo);
                channel.Close();
            }
        }
        private void InternalUpload(IChannelSession channel, Stream input, FileInfo fileInfo, string filename)
        {
            this.InternalSetTimestamp(channel, input, fileInfo.LastWriteTimeUtc, fileInfo.LastAccessTimeUtc);
            using (var source = fileInfo.OpenRead())
            {
                this.InternalUpload(channel, input, source, filename);
            }
        }
        private void InternalUpload(IChannelSession channel, Stream input, DirectoryInfo directoryInfo)
        {
            //  Upload files
            var files = directoryInfo.GetFiles();
            foreach (var file in files)
            {
                this.InternalUpload(channel, input, file, file.Name);
            }
            //  Upload directories
            var directories = directoryInfo.GetDirectories();
            foreach (var directory in directories)
            {
                this.InternalSetTimestamp(channel, input, directoryInfo.LastWriteTimeUtc, directoryInfo.LastAccessTimeUtc);
                this.SendData(channel, string.Format("D0755 0 {0}\n", directory.Name));
                this.CheckReturnCode(input);
                this.InternalUpload(channel, input, directory);
                this.SendData(channel, "E\n");
                this.CheckReturnCode(input);
            }
        }
        private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo)
        {
            DateTime modifiedTime = DateTime.Now;
            DateTime accessedTime = DateTime.Now;
            var startDirectoryFullName = fileSystemInfo.FullName;
            var currentDirectoryFullName = startDirectoryFullName;
            var directoryCounter = 0;
            while (true)
            {
                var message = ReadString(input);
                if (message == "E")
                {
                    this.SendConfirmation(channel); //  Send reply
                    directoryCounter--;
                    currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName;
                    if (directoryCounter == 0)
                        break;
                    continue;
                }
                var match = _directoryInfoRe.Match(message);
                if (match.Success)
                {
                    this.SendConfirmation(channel); //  Send reply
                    //  Read directory
                    var mode = long.Parse(match.Result("${mode}"));
                    var filename = match.Result("${filename}");
                    DirectoryInfo newDirectoryInfo;
                    if (directoryCounter > 0)
                    {
                        newDirectoryInfo = Directory.CreateDirectory(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, filename));
                        newDirectoryInfo.LastAccessTime = accessedTime;
                        newDirectoryInfo.LastWriteTime = modifiedTime;
                    }
                    else
                    {
                        //  Dont create directory for first level
                        newDirectoryInfo = fileSystemInfo as DirectoryInfo;
                    }
                    directoryCounter++;
                    currentDirectoryFullName = newDirectoryInfo.FullName;
                    continue;
                }
                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}");
                    var fileInfo = fileSystemInfo as FileInfo;
                    if (fileInfo == null)
                        fileInfo = new FileInfo(string.Format("{0}{1}{2}", currentDirectoryFullName, Path.DirectorySeparatorChar, fileName));
                    using (var output = fileInfo.OpenWrite())
                    {
                        this.InternalDownload(channel, input, output, fileName, length);
                    }
                    fileInfo.LastAccessTime = accessedTime;
                    fileInfo.LastWriteTime = modifiedTime;
                    if (directoryCounter == 0)
                        break;
                    continue;
                }
                match = _timestampRe.Match(message);
                if (match.Success)
                {
                    //  Read timestamp
                    this.SendConfirmation(channel); //  Send reply
                    var mtime = long.Parse(match.Result("${mtime}"));
                    var atime = long.Parse(match.Result("${atime}"));
                    var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
                    modifiedTime = zeroTime.AddSeconds(mtime);
                    accessedTime = zeroTime.AddSeconds(atime);
                    continue;
                }
                this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
            }
        }
        partial void SendData(IChannelSession channel, string command)
        {
            channel.SendData(System.Text.Encoding.Default.GetBytes(command));
        }
    }
}