using System;
using System.Collections.Generic;
using System.Threading;
using Bitvise;
using Bitvise.FlowSshNet;

namespace FlowSshNet_SftpStress
{
    enum XferVerb { Put, Get };

    class XferCommand
    {
        public XferVerb m_verb;
        public string   m_localPath;
        public string   m_remotePath;
    }

    class Coordination
    {
        public ManualResetEvent m_stopEvent = new ManualResetEvent(false);
        public string m_stopReason;

        public void Stop(string reason)
        {
            lock (this)
            {
                if (m_stopReason == null)
                    m_stopReason = reason;

                m_stopEvent.Set();
            }
        }
    }

    class XferErr : System.Exception
    {
        public XferErr(string msg) : base(msg) {}
    }

    class XferThread
    {
        public Coordination m_coord;
        public uint         m_threadNr;

        public string       m_host;
        public uint         m_port;
        public string       m_hkfp;
        public string       m_user;
        public string       m_pass;
        public XferCommand  m_cmd;

        public void Run()
        {
            try
            {
                Console.WriteLine("Thread {0} started", m_threadNr);

                while (true)
                {
                    using (Client client = new Client())
                    {
                        client.SetAppName("FlowSshNet_SftpStress");

                        client.SetHost(m_host);
                        client.SetPort(m_port);
                        client.SetUserName(m_user);
                        client.SetPassword(m_pass);
                        client.AcceptHostKeySha256(m_hkfp);

                        Console.WriteLine("Thread {0} connecting", m_threadNr);
                        
                        ProgressHandler ph = new ProgressHandler();
                        client.Connect(ph);
                        ph.WaitDone();
                        if (!ph.Success)
                            throw new XferErr(ph.DescribeConnectError());

                        using (ClientSftpChannel sftp = new ClientSftpChannel(client))
                        {
                            ph = new ProgressHandler();
                            sftp.Open(ph);
                            ph.WaitDone();
                            if (!ph.Success)
                                throw new XferErr(ph.DescribeSftpChannelOpenError());

                            TransferHandler th = new TransferHandler();

                            if (m_cmd.m_verb == XferVerb.Put)
                            {
                                Console.WriteLine("Thread {0} putting {1} -> {2}", m_threadNr, m_cmd.m_localPath, m_cmd.m_remotePath);
                                sftp.Upload(m_cmd.m_localPath, m_cmd.m_remotePath, TransferFlags.Binary | TransferFlags.Overwrite, th);
                            }
                            else
                            {
                                Console.WriteLine("Thread {0} getting {1} -> {2}", m_threadNr, m_cmd.m_remotePath, m_cmd.m_localPath);
                                sftp.Download(m_cmd.m_remotePath, m_cmd.m_localPath, TransferFlags.Binary | TransferFlags.Overwrite, th);
                            }

                            th.WaitDone();
                            if (!th.Success)
                                throw new XferErr(th.GetError().Describe());

                            Console.WriteLine("Thread {0} transferred {1} bytes", m_threadNr, th.GetTransferStat().BytesTransferred);
                        }

                        Console.WriteLine("Thread {0} disconnecting", m_threadNr);

                        ph = new ProgressHandler();
                        client.Disconnect(ph);
                        ph.WaitDone();
                    }

                    if (m_coord.m_stopEvent.WaitOne(0))
                        break;
                }
            }
            catch (System.Exception e)
            {
                m_coord.Stop("XferThread " + m_threadNr + " exited with exception:\r\n" + e.ToString());
            }
            finally
            {
                m_coord.Stop("XferThread " + m_threadNr + " exited without exception");
            }
        }
    }

    class FlowSshNet_SftpStress
    {
        static void OnUncaughtExceptionInEvent(object sender, bool fatal, System.Exception e)
        {
            if (fatal) Console.WriteLine("Error [fatal] in event: " + e.ToString());
            else       Console.WriteLine("Error in event: " + e.ToString());
            System.Environment.Exit(3);
        }

        static int Main(string[] args)
        {
            // Register delegate for uncaught exceptions in user-defined events.
            // Example: If there is an uncaught exception in MyClient.OnMyHostKey, 
            //          then this is reported in OnUncaughtExceptionInEvent.
            SshNet.OnExceptionInEvent += new ExceptionInEventEventHandler(OnUncaughtExceptionInEvent);

            if (args.Length < 6)
            {
                Console.WriteLine("Usage: <host> <port> <hostKeySha256> <user> <pass> <thread1command> [; <thread2command> ... ]");
                Console.WriteLine("A thread command can be either a put or get:");
                Console.WriteLine("  put \"localFilePath\" \"remoteFilePath\"");
                Console.WriteLine("  get \"remoteFilePath\" \"localFilePath\"");
                Console.WriteLine("Thread commands should not conflict, or have inter-dependencies.");
                Console.WriteLine("Each command will be executed repeatedly and simultaneously on a separate thread.");
                return 2;
            }

            string host = args[0];
            uint   port = uint.Parse(args[1]);
            string hkfp = args[2];
            string user = args[3];
            string pass = args[4];

            List<XferCommand> commands = new List<XferCommand>();
            for (int i=5; ; )
            {
                XferVerb verb;
                     if (args[i] == "put") verb = XferVerb.Put;
                else if (args[i] == "get") verb = XferVerb.Get;
                else { Console.WriteLine("Unrecognized command verb"); return 2; }

                if (++i == args.Length) { Console.WriteLine("Missing command path 1"); return 2; }
                string path1 = args[i];

                if (++i == args.Length) { Console.WriteLine("Missing command path 2"); return 2; }
                string path2 = args[i];

                XferCommand cmd = new XferCommand();
                cmd.m_verb       = verb;
                cmd.m_localPath  = (verb == XferVerb.Put ? path1 : path2);
                cmd.m_remotePath = (verb == XferVerb.Put ? path2 : path1);
                commands.Add(cmd);

                if (++i == args.Length) break;
                if (args[i] != ";")     { Console.WriteLine("Expecting ; between commands"); return 2; }
                if (++i == args.Length) { Console.WriteLine("Expecting command after ; separator"); return 2; }
            }

            Console.WriteLine("Starting threads. Press Esc to exit");

            Coordination coord    = new Coordination();
            List<Thread> threads  = new List<Thread>();
            uint         threadNr = 1;

            foreach (XferCommand cmd in commands)
            {
                XferThread xt = new XferThread();
                xt.m_coord    = coord;
                xt.m_threadNr = threadNr++;
                xt.m_host     = host;
                xt.m_port     = port;
                xt.m_hkfp     = hkfp;
                xt.m_user     = user;
                xt.m_pass     = pass;
                xt.m_cmd      = cmd;

                Thread t = new Thread(xt.Run);
                t.Start();
                threads.Add(t);
            }

            // Wait Esc key or child thread exit
            while (true)
            {
                while (Console.KeyAvailable)
                    if (Console.ReadKey(true).KeyChar == 27)
                    {
                        coord.Stop("Esc key pressed");
                        break;
                    }

                if (coord.m_stopEvent.WaitOne(200))
                    break;
            }

            Console.WriteLine("Stopping...");
            foreach (Thread t in threads)
                t.Join();

            if (coord.m_stopReason != null)
                Console.WriteLine("Stopped: " + coord.m_stopReason);

            return 0;
        }
    }
}
