//  
//   Rebex Sample Code License
// 
//   Copyright 2025, REBEX CR s.r.o.
//   All rights reserved.
//   https://www.rebex.net/
// 
//   Permission to use, copy, modify, and/or distribute this software for any
//   purpose with or without fee is hereby granted.
// 
//   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
//   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
//   OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
//   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
//   HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
//   WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
//   FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
//   OTHER DEALINGS IN THE SOFTWARE.
// 

using System;
using System.Text;
using System.IO;

using Rebex.IO;
using Rebex.Net;

namespace Rebex.Samples
{
    /// <summary>
    /// This sample is a simple command line FTP over TLS/SSL client.
    /// In addition to basic operations, it demonstrates the following features:
    /// - extending FtpItem and using a custom list parser to get Unix rights
    /// - using a "Site" method for changing Unix rights using chmod command
    /// - using FtpItemCollection class
    /// </summary>
    class ConsoleClient
    {

        public static string ReadLine()
        {
            StringBuilder line = new StringBuilder();
            bool complete = false;

            while (!complete)
            {
                ConsoleKeyInfo key = Console.ReadKey(true);
                switch (key.Key)
                {
                    case ConsoleKey.Enter:
                        complete = true;
                        break;

                    case ConsoleKey.Backspace:
                        if (line.Length > 0)
                        {
                            line = line.Remove(line.Length - 1, 1);
                            Console.Write(key.KeyChar);
                            Console.Write(" ");
                            Console.Write(key.KeyChar);
                        }
                        break;

                    default:
                        if (((int)key.KeyChar >= 0x20) || (key.Key == ConsoleKey.Tab))
                        {
                            line = line.Append(key.KeyChar);
                            Console.Write("*");
                        }
                        break;
                }
            }

            Console.WriteLine();
            return line.ToString();
        }

        private DateTime lastReport = DateTime.MinValue;

        public void TransferProgressChanged (object sender, FtpTransferProgressChangedEventArgs e)
        {
            if (e.TransferState != TransferProgressState.DataBlockProcessed)
                return;

            if ((DateTime.Now - lastReport).TotalSeconds > 1)
            {
                Console.WriteLine (e.BytesTransferred + " bytes.");
                lastReport = DateTime.Now;
            }
        }

        private Ftp ftp;

        public ConsoleClient ()
        {
            // create Ftp object and set response handler
            ftp = new Ftp();
            ftp.LogWriter = new ConsoleLogWriter(LogLevel.Info);
            ftp.TransferProgressChanged += TransferProgressChanged;
        }

        private void Help ()
        {
            Console.WriteLine ("!         disconnect   rmdir       rmtree");
            Console.WriteLine ("?         exit         user        auth");
            Console.WriteLine ("ascii     help         chmod       ccc");
            Console.WriteLine ("active    ls           get");
            Console.WriteLine ("binary    mkdir        put");
            Console.WriteLine ("bye       open         getr");
            Console.WriteLine ("cd        passive      putr");
            Console.WriteLine ("close     pwd          putdir");
            Console.WriteLine ("dir       quit         getdir");
        }

        private void Close ()
        {
            Console.WriteLine ("Disconnecting...");
            ftp.Disconnect();
        }

        private bool Open (string host)
        {
            try
            {

                if (ftp.State != FtpState.Disconnected)
                {
                    Console.WriteLine ("Already connected. Disconnect first.");
                    return false;
                }

                if (host == null || host.Trim().Length == 0)
                {
                    Console.Write ("Hostname: ");
                    host = Console.ReadLine();
                }

                string[] p = host.Split(' ');
                if (p.Length<0 || p.Length>2)
                {
                    Console.WriteLine ("Usage: hostname [port]");
                    return false;
                }

                host = p[0];
                int port;
                if (p.Length == 1)
                {
                    port = Ftp.DefaultPort;
                }
                else
                {
                    try
                    {
                        port = int.Parse (p[1]);
                    }
                    catch
                    {
                        port = -1;
                    }
                    if (port <= 0 || port > 65535)
                    {
                        Console.WriteLine ("Invalid port number.");
                        return false;
                    }
                }

                ftp.Connect (host, port);
                return true;
            }
            catch (FtpException e)
            {
                Console.WriteLine (e.Message);
                return false;
            }
        }

        private bool Auth ()
        {
            try
            {
                ftp.Settings.SslClientCertificateRequestHandler = new RequestHandler();
                ftp.ValidatingCertificate += new EventHandler<SslCertificateValidationEventArgs>(ConsoleVerifier.ValidatingCertificate);
                ftp.Secure();
                return true;
            }
            catch (TlsException e)
            {
                Console.WriteLine (e.Message);
                Console.WriteLine ("TLS negotiation failed, disconnecting.");
                ftp.Disconnect ();
                return false;
            }
            catch (FtpException e)
            {
                Console.WriteLine (e.Message);
                if (e.Response == null)
                {
                    Console.WriteLine ("TLS negotiation failed.");
                    Console.WriteLine (e);
                    return false;
                }
                if (e.Response.Code == 500)
                {
                    Console.WriteLine ("Server does not support explicit SSL/TLS security.");
                    return true;
                }
                return false;
            }
        }

        private void ClearCommandChannel()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (!ftp.IsSecured)
            {
                Console.WriteLine("The control connection is not secured.");
                return;
            }

            ftp.ClearCommandChannel();
        }
        private void Login (string user)
        {
            if (user == null || user.Length == 0)
            {
                Console.Write ("User: ");
                user = Console.ReadLine();
            }

            Console.Write ("Password: ");
            string pass = ReadLine();

            ftp.Login (user, pass);
        }

        private void Pwd ()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.GetCurrentDirectory();
        }

        private void Chmod (string param)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: chmod mode filename");
                return;
            }

            ftp.Site ("CHMOD " + param);
        }


        private void Cd (string param)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.ChangeDirectory (param);
        }

        private void Quit ()
        {
            if (ftp.State != FtpState.Disconnected)
                Close ();

            Console.WriteLine ();
        }

        private void Ascii ()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.TransferType = FtpTransferType.Ascii;
        }

        private void Active ()
        {
            ftp.Passive = false;
            Console.WriteLine ("Passive mode turned off.");
        }

        private void Passive ()
        {
            ftp.Passive = true;
            Console.WriteLine ("Passive mode turned on.");
        }

        private void Binary ()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            ftp.TransferType = FtpTransferType.Binary;
        }

        private void Mkdir (string param)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: mkdir dirname");
                return;
            }

            ftp.CreateDirectory (param);
        }

        private void Rmdir (string param)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: rmdir dirname");
                return;
            }

            ftp.RemoveDirectory (param);
        }

        /// <summary>
        /// Removes a whole directory tree. Use with care.
        /// </summary>
        /// <param name="ftp">An instance of Ftp object.</param>
        /// <param name="path">A path to a directory to remove.</param>
        public static void RemoveTree (Ftp ftp, string path)
        {
            // Save the current directory and change it to 'path'.
            string original = ftp.GetCurrentDirectory ();
            ftp.ChangeDirectory (path);

            // Get this list of files and directories
            FtpItemCollection list = ftp.GetList ();
            
            // Delete all files and directories in the list recursively
            for (int i=0; i<list.Count; i++)
            {
                if (list[i].IsDirectory)
                    RemoveTree (ftp, list[i].Name);
                else
                    ftp.DeleteFile (list[i].Name);
            }

            // Go back to the original directory
            ftp.ChangeDirectory (original);

            // Remove the directory that was just cleaned
            ftp.RemoveDirectory (path);
        }

        private void Rmtree (string param)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (param == null || param.Length == 0)
            {
                Console.WriteLine ("Usage: rmtree dirname");
                return;
            }

            RemoveTree (ftp, param);
        }

        private void Dir ()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            lastReport = DateTime.Now;
            FtpItemCollection list = ftp.GetList ();

            for (int i=0; i<list.Count; i++)
            {
                FtpItem item = list[i];
                if (item.IsLink)
                    Console.Write ("s ");
                else if (item.IsDirectory)
                    Console.Write ("d ");
                else
                    Console.Write ("- ");

                if (item.Permissions != null)
                    Console.Write (Convert.ToString ((int)item.Permissions.Value, 8).PadLeft(3,'0') + " ");
                else
                    Console.Write ("??? ");

                Console.Write (item.LastWriteTime.Value.ToString("u").Substring(0,16));
                Console.Write (item.Length.ToString().PadLeft(10,' '));
                Console.Write (" {0}", item.Name);
                if (item.IsLink)
                    Console.Write (" -> " + item.SymlinkPath);
                Console.WriteLine ();
            }
        }

        private void Ls ()
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            lastReport = DateTime.Now;
            FtpItemCollection list = ftp.GetList ();

            for (int i=0; i<list.Count; i++)
            {
                Console.WriteLine (list[i].Name);
            }
        }

        private void Get (string remotePath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: get remotePath");
                return;
            }

            try
            {
                lastReport = DateTime.Now;
                ftp.GetFile (remotePath, Path.GetFileName(remotePath));
            }
            catch (Exception e)
            {
                if (e is FtpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }


        private void GetResume (string remotePath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: getr remotePath");
                return;
            }

            FileStream localStream = null;
            try
            {
                // Get local file name corresponding to remote file name.
                string localPath = Path.GetFileName(remotePath);

                // Get the length of remote file. This will throw exception
                // if the file does not exists.
                long remoteLength = ftp.GetFileLength (remotePath);

                // Create or open the local file for writing and seek to the end.
                localStream = File.Open (localPath, FileMode.Append, FileAccess.Write, FileShare.Read);

                // Check lengths too see if the resume makes sense.
                if (localStream.Length == remoteLength)
                {
                    Console.WriteLine ("File has already been downloaded.");
                }
                else if (localStream.Length > remoteLength)
                {
                    Console.WriteLine ("Local file is longer than remote file.");
                }
                else
                {
                    // Display resume info if we are really resuming download.
                    if (localStream.Length > 0)
                        Console.WriteLine ("Download resumed at offset " + localStream.Length + ".");

                    lastReport = DateTime.Now;

                    // Download the file. Start at the position of the length of the local stream.
                    ftp.GetFile (remotePath, localStream, localStream.Length);
                }
            }
            catch (Exception e)
            {
                if (e is FtpException)
                    throw;

                Console.WriteLine (e.Message);
            }
            finally
            {
                // Close the stream if it was created.
                if (localStream != null)
                    localStream.Close ();
            }
        }

        /// <summary>
        /// Uploads a local directory with all files and subdirectories to an FTP server.
        /// </summary>
        /// <param name="ftp">Instance of an Ftp object.</param>
        /// <param name="localPath">The local path of the root of the directory structure to be uploaded.</param>
        /// <remarks>
        /// <p>
        /// Ftp object must be connected and should have the directory set to the desired path.
        /// </p>
        /// <p>
        /// Method reports information to console. This behaviour should be changed to reflect actual needs.
        /// </p>
        /// </remarks>
        public static void UploadDirectory (Ftp ftp, string localPath)
        {
            // strip ending slashes (GetFileName returns empty string otherwise...)
            localPath = localPath.TrimEnd ('\\','/');

            Console.WriteLine ("Uploading directory: '" + localPath + "'");

            // get the name of the directory and try to create it
            string remotePath = Path.GetFileName (localPath);
            try
            {
                ftp.CreateDirectory (remotePath);
            }
            catch (FtpException e)
            {
                // throw exception if an error other than ProtocolError occurs
                if (e.Status != FtpExceptionStatus.ProtocolError)
                    throw;

                // protocol error - directory probably already exists
                Console.WriteLine (e.Message);
            }
            
            // change the directory
            ftp.ChangeDirectory (remotePath);

            // upload all files from the localPath directory
            string[] files = Directory.GetFiles (localPath);
            for (int i=0; i<files.Length; i++)
            {
                Console.WriteLine ("Uploading file: '" + files[i] + "'");
                try
                {
                    ftp.PutFile (files[i], Path.GetFileName (files[i]));
                }
                catch (FtpException e)
                {
                    // throw exception if an error other than ProtocolError occurs
                    if (e.Status != FtpExceptionStatus.ProtocolError)
                        throw;

                    // protocol error - report it and continue with the remaining files
                    Console.WriteLine (e.Message);
                }
            }

            // upload all directories from the localPath directory
            string[] dirs = Directory.GetDirectories (localPath);
            for (int i=0; i<dirs.Length; i++)
                UploadDirectory (ftp, dirs[i]);

            ftp.ChangeDirectory ("..");
        }

        private void PutDir (string localPath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: putdir localPath");
                return;
            }

            if (!Directory.Exists (localPath))
            {
                Console.WriteLine ("Path does not exist.");
                return;
            }

            lastReport = DateTime.Now;
            UploadDirectory (ftp, localPath);
        }

        public static void DownloadDirectory (Ftp ftp, string remotePath)
        {
            // strip ending slashes (GetFileName returns empty string otherwise...)
            remotePath = remotePath.TrimEnd ('\\','/');

            Console.WriteLine ("Downloading directory: '" + remotePath + "'");

            // get the name of the directory and try to create it
            string localPath = Path.GetFileName (remotePath);
            try
            {
                Directory.CreateDirectory (localPath);
            }
            catch (Exception e)
            {
                // error - directory probably already exists
                Console.WriteLine (e.Message);
            }
            
            // change the directory
            Directory.SetCurrentDirectory (localPath);
            ftp.ChangeDirectory (remotePath);

            // download all files from the remotePath directory
            FtpItemCollection files = ftp.GetList ();
            for (int i=0; i<files.Count; i++)
            {
                if (files[i].IsFile)
                {
                    Console.WriteLine ("Downloading file: '" + files[i].Name + "'");
                    try
                    {
                        if (!File.Exists (files[i].Name))
                            ftp.GetFile (files[i].Name, files[i].Name);
                    }
                    catch (FtpException e)
                    {
                        // throw exception if an error other than ProtocolError occurs
                        if (e.Status != FtpExceptionStatus.ProtocolError)
                            throw;

                        // protocol error - report it and continue with the remaining files
                        Console.WriteLine (e.Message);
                    }
                }
            }

            // download all directories from the remotePath directory
            for (int i=0; i<files.Count; i++)
            {
                if (files[i].IsDirectory)
                    DownloadDirectory (ftp, files[i].Name);
            }

            Directory.SetCurrentDirectory ("..");
            ftp.ChangeDirectory ("..");
        }

        private void GetDir (string remotePath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (remotePath == null || remotePath.Length == 0)
            {
                Console.WriteLine ("Usage: getdir remotePath");
                return;
            }

            lastReport = DateTime.Now;
            DownloadDirectory (ftp, remotePath);
        }

        private void Put (string localPath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: put localPath");
                return;
            }

            try
            {
                lastReport = DateTime.Now;
                ftp.PutFile (localPath, Path.GetFileName(localPath));
            }
            catch (Exception e)
            {
                if (e is FtpException)
                    throw;

                Console.WriteLine (e.Message);
            }
        }

        private void PutResume (string localPath)
        {
            if (ftp.State == FtpState.Disconnected)
            {
                Console.WriteLine ("Not connected.");
                return;
            }

            if (localPath == null || localPath.Length == 0)
            {
                Console.WriteLine ("Usage: putr localPath");
                return;
            }

            FileStream localStream = null;
            try
            {
                // Get local file name corresponding to remote file name.
                string remotePath = Path.GetFileName (localPath);
                
                // Try to get the length of the remote file. If the file
                // does not exist, it will throw exception with status
                // of ProtocolError and response code 550. Ignore this
                // exception and report the others.
                long remoteLength = 0;
                try
                {
                    remoteLength = ftp.GetFileLength (remotePath);
                }
                catch (FtpException e)
                {
                    if (e.Status != FtpExceptionStatus.ProtocolError || e.Response.Code != 550)
                        throw;
                }

                // Open the local file for reading.
                localStream = File.OpenRead (localPath);

                // Check lengths too see if the resume makes sense.
                if (localStream.Length == remoteLength)
                {
                    Console.WriteLine ("File has already been uploaded.");
                }
                else if (localStream.Length < remoteLength)
                {
                    Console.WriteLine ("Remote file is longer than local file.");
                }
                else
                {
                    // Display resume info if we are really resuming upload.
                    if (remoteLength > 0)
                        Console.WriteLine ("Upload resumed at offset " + remoteLength + ".");
                    
                    lastReport = DateTime.Now;

                    // Upload the file. Start at the position of the length of the remote stream.
                    localStream.Position = remoteLength;
                    ftp.PutFile (localStream, remotePath, remoteLength, -1);
                }
            }
            catch (Exception e)
            {
                if (e is FtpException)
                    throw;

                Console.WriteLine (e.Message);
            }
            finally
            {
                // Close the stream if it was created.
                if (localStream != null)
                    localStream.Close ();
            }
        }

        public void Run ()
        {
            while (true)
            {
                Console.Write ("ftp> ");
                string command = Console.ReadLine().Trim();
                string param = null;
                int i = command.IndexOf (' ');
                if (i > 0)
                {
                    param = command.Substring (i + 1);
                    command = command.Substring (0, i);
                }

                try
                {
                    switch (command.ToLower())
                    {
                        case "!":
                            goto case "quit";
                        case "?":
                            goto case "help";
                        case "bye":
                            goto case "quit";
                        case "exit":
                            goto case "quit";
                        case "disconnect":
                            goto case "close";
                        case "quit":
                            Quit();
                            return;
                        case "close":
                            Close();
                            break;
                        case "open":
                            if (!Open (param)) break;
                            param = null;
                            goto case "auth";
                        case "auth":
                            if (!Auth ()) break;
                            goto case "user";
                        case "user":
                            Login (param);
                            break;
                        case "help":
                            Help();
                            break;
                        case "ccc":
                            ClearCommandChannel();
                            break;
                        case "ascii":
                            Ascii();
                            break;
                        case "binary":
                            Binary();
                            break;
                        case "active":
                            Active();
                            break;
                        case "passive":
                            Passive();
                            break;
                        case "cd":
                            Cd (param);
                            break;
                        case "pwd":
                            Pwd();
                            break;
                        case "chmod":
                            Chmod (param);
                            break;
                        case "mkdir":
                            Mkdir (param);
                            break;
                        case "rmdir":
                            Rmdir (param);
                            break;
                        case "rmtree":
                            Rmtree (param);
                            break;
                        case "get":
                            Get (param);
                            break;
                        case "put":
                            Put (param);
                            break;
                        case "getr":
                            GetResume (param);
                            break;
                        case "putr":
                            PutResume (param);
                            break;
                        case "putdir":
                            PutDir (param);
                            break;
                        case "getdir":
                            GetDir (param);
                            break;
                        case "dir":
                            Dir();
                            break;
                        case "ls":
                            if (param != null && param.StartsWith ("-l"))
                                Dir();
                            else
                                Ls();
                            break;
                        default:
                            Console.WriteLine ("Invalid command.");
                            break;
                    }
                }
                catch (FtpException e)
                {
                    if (e.Status != FtpExceptionStatus.ProtocolError && e.Status != FtpExceptionStatus.ConnectionClosed)
                    {
                        Console.WriteLine (e.ToString());
                        ftp.Disconnect();
                    }
                }

            }
        }

        static void Main (string[] args)
        {
            // set the trial license key
            Rebex.Licensing.Key = Rebex.Samples.TrialKey.Key;


            ConsoleClient client = new ConsoleClient ();

            if (args.Length > 0)
            {
                string param = args[0];
                for (int i=1; i<args.Length; i++)
                    param += " " + args[i];

                if (client.Open (param))
                    client.Login (null);
            }

            client.Run ();
        }
    }
}
