//  
//   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.Drawing;
using System.Web;
using System.Web.UI;

using Rebex.Samples.Code;

using Rebex;
using Rebex.Net;
using Rebex.IO;

namespace Rebex.Samples
{
    public partial class _Default : Page
    {
        #region Private fields

        // connection information
        private ConnectionInfo _connectionInfo;

        // path to the current directory 
        private string _currentDirectory;

        // an Sftp object used to communicate with remote SFTP server
        private Sftp _sftp;

        #endregion

        #region Properties

        /// <summary>
        /// Gets the path to the current directory.
        /// </summary>
        public string CurrentDirectory
        {
            get
            {
                return _currentDirectory;
            }
        }

        #endregion

        #region Event handlers for buttons

        /// <summary>
        /// Handles the 'Upload' button click.
        /// </summary>
        protected void UploadFile(object sender, EventArgs e)
        {
            UploadFile();
        }

        #endregion

        protected void Page_Load(object sender, EventArgs e)
        {
            // load saved ConnectionInfo from the Session
            _connectionInfo = ConnectionInfo.Load();

            // Ensure, that we have all information needed for establishing connection to the SFTP server.
            // Redirect to login page if connection info is not set 
            // (e.g. this page was visited for the first time or session has expired or logout button was clicked)
            if (Request.QueryString.Count == 0)
                Response.Redirect("Login.aspx");
            if (_connectionInfo == null)
                Response.Redirect(string.Format("Login.aspx?{0}={1}",
                    UrlParameter.ReturnUrl, Uri.EscapeDataString(Request.Url.PathAndQuery)));

            // PostBack events logic is done in their handlers
            if (!Page.IsPostBack)
                ProcessCommand();
        }

        /// <summary>
        /// Reads HttpRequest parameters, connects to the SFTP server, executes requested command and disconnects from the server.
        /// </summary>
        private void ProcessCommand()
        {
            try
            {
                // connect to the SFTP server
                ConnectSftp();

                // execute requested SFTP command
                ExecuteSftpCommand();
            }
            catch (ApplicationException ex)
            {
                // show the error message
                lblError.Text = ex.Message;
            }
            catch (SftpException ex)
            {
                // show the error message
                lblError.Text = ex.Message;
            }
            finally
            {
                if (_sftp != null)
                {
                    // close connect if connected
                    if (_sftp.State == SftpState.Ready)
                        _sftp.Disconnect();

                    // dispose the oject
                    _sftp.Dispose();
                    _sftp = null;
                }
            }
        }

        /// <summary>
        /// Executes requested SFTP command. 
        /// </summary>
        /// <remarks>
        /// SFTP Command and required parameters are taken from HttpRequest. The
        /// method requires fully connected and logged in _sftp object.
        /// </remarks>
        private void ExecuteSftpCommand()
        {
            // requested SFTP command
            string command = Request[UrlParameter.Command];

            // choose appropriate action according to the requested command
            switch (command)
            {
                case UrlParameterCommand.List:
                    ShowFileList();
                    break;
                case UrlParameterCommand.Download:
                    DownloadFile();
                    break;
                case UrlParameterCommand.DeleteFile:
                    DeleteFile();
                    break;
                case UrlParameterCommand.CreateDirectory:
                    CreateDirectory();
                    break;
                case UrlParameterCommand.DeleteDirectory:
                    DeleteDirectory();
                    break;
                case UrlParameterCommand.Rename:
                    Rename();
                    break;
                case UrlParameterCommand.Symlink:
                    ResolveSymlink();
                    break;
                case UrlParameterCommand.LogOut:
                    LogOut();
                    break;
                default:
                    throw new InvalidOperationException(string.Format("Unknown COMMAND '{0}'.", command));
            }
        }

        /// <summary>
        /// Receives the directory list and binds it to the grid view.
        /// </summary>
        private void ShowFileList()
        {
            // set current directory
            _sftp.ChangeDirectory(_currentDirectory);

            // get list of the current directory
            SftpItemCollection list = _sftp.GetList();

            // sort list, file type first, then by name
            list.Sort(new MultiComparer(
                new FileSystemItemComparer(FileSystemItemComparerType.FileType, SortingOrder.Ascending),
                new FileSystemItemComparer(FileSystemItemComparerType.Name, SortingOrder.Ascending))
            );

            // bind GridView
            gwList.DataSource = list;
            gwList.DataBind();
        }


        /// <summary>
        /// Parses the HTTP Range header.
        /// </summary>
        /// <param name="range">The HTTP Range header.</param>
        /// <param name="length">Resource length.</param>
        /// <param name="from">First byte position.</param>
        /// <param name="to">Last byte position.</param>
        /// <returns>True if parsing was succesful; otherwise false.</returns>
        private bool ParseRange(string range, long length, out long from, out long to)
        {
            from = -1;
            to = -1;

            if (range == null || !range.StartsWith("bytes="))
                return false;

            range = range.Substring(6);

            int index;
            if ((index = range.IndexOf(",")) >= 0)
                range = range.Substring(0, index);

            string[] ranges = range.Split('-');

            if (ranges.Length != 2)
                return false;

            if (!long.TryParse(ranges[0], out from))
            {
                if (ranges[0] != string.Empty)
                    return false;

                from = -1;
            }
                
            if (!long.TryParse(ranges[1], out to))
            {
                if (ranges[1] != string.Empty)
                    return false;

                to = -1;
            }

            if (from == -1)
            {
                if (to == -1)
                    return false;

                from = length - to;
                to = length - 1;
            }
            else if (to == -1)
            {
                to = length - 1;
            }

            return true;
        }

        /// <summary>
        /// Downloads a file from the SFTP server to asp.net server. 
        /// Then sends the file to client's web browser via HTTP.
        /// </summary>
        private void DownloadFile()
        {
            // Communication scheme:
            //
            // 1. Web browser connects to ASP.NET web server and requests
            //    file download.
            //
            // 2. ASP.NET server connects to SFTP server and requests the download.
            //
            // 3. SFTP server starts sending the file back to ASP.NET server.
            //
            // 4. ASP.NET page starts receiving the file. All received data are 
            //    immediately sent to web browser on client machine.
            //
            // 5. ASP.NET closes the connection to the SFTP server.
            // 

            // absolute path to the file
            string remotePath = Request[UrlParameter.UrlCommandArgument1];
            string range = Request.Headers["Range"];

            // prepare name of the file
            string fileName = GetFileName(remotePath);
            long length = _sftp.GetFileLength(remotePath);

            // clear response stream and set response header and content type
            Response.Clear();
            Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}", fileName));
            Response.ContentType = "application/octet-stream";
            Response.AddHeader("Accept-Ranges", "bytes");
            Response.BufferOutput = false;

            long from, to;
            if (range == null)
            {
                // transfer the whole file (no range specified)

                Response.AddHeader("Content-Length", length.ToString());

                // write the file content to the response
                _sftp.GetFile(remotePath, Response.OutputStream);
            }
            else if (ParseRange(range, length, out from, out to) && from >= 0 && from < length && to < length && from <= to)
            {
                // transfer the requested file part

                long contentLength = to - from + 1;
                Response.AddHeader("Content-Length", contentLength.ToString());
                Response.AddHeader("Content-Range", string.Format("bytes {0}-{1}/{2}", from, to, length));
                Response.Status = "206 Partial Content";

                // write the file content to the response
                _sftp.GetFile(remotePath, Response.OutputStream, from, contentLength);
            }
            else
            {
                // report an error
                Response.AddHeader("Content-Range", string.Format("bytes */{0}", length));
                Response.Status = "416 Range Not Satisfiable";
            }

            // close connection to the SFTP server
            _sftp.Disconnect();

            Response.End();
        }

        /// <summary>
        /// Uploads a file.
        /// </summary>
        private void UploadFile()
        {
            // Communication scheme:
            // 
            // 1. Web browser connects to ASP.NET web server and sends file
            //    via FileUpload control.
            // 
            // 2. ASP.NET server connects to SFTP server
            // 
            // 3. ASP.NET server starts receiving the file from the web browsers.
            //    All received data are immediately sent to SFTP server.
            // 
            // 4. ASP.NET closes the connection to the SFTP server.

            try
            {
                // connect to the SFTP server
                ConnectSftp();

                // FileUpload control has the file ready
                if (fileToUpload.HasFile)
                {
                    // upload the file
                    string remotePath = PathCombine(_currentDirectory, fileToUpload.FileName);
                    _sftp.PutFile(fileToUpload.FileContent, remotePath);

                    // show succes message
                    lblSuccess.Text = "File was successfully uploaded.";

                    // reload the directory list
                    ShowFileList();
                }
                else
                {
                    // no file has been uploaded using FileUpload control, show the error message
                    lblError.Text = "No file to upload.";
                }
                // disconnect from the SFTP server
                _sftp.Disconnect();
            }
            catch (SftpException ex)
            {
                // show error message
                lblError.Text = ex.Message;
            }
            finally
            {
                // dispose the Sftp object
                if (_sftp != null)
                    _sftp.Dispose();
            }

        }

        /// <summary>
        /// Deletes the file.
        /// </summary>
        private void DeleteFile()
        {
            // absolute path to the file
            string remotePath = Request[UrlParameter.UrlCommandArgument1];

            // delete the file
            _sftp.DeleteFile(remotePath);

            ShowFileList();
        }

        /// <summary>
        /// Creates a new directory.
        /// </summary>
        private void CreateDirectory()
        {
            // new direcoty name
            string name = Request[UrlParameter.UrlCommandArgument1];

            // create the directory
            _sftp.CreateDirectory(PathCombine(_currentDirectory, name));

            ShowFileList();
        }

        /// <summary>
        /// Deletes the directory.
        /// </summary>
        private void DeleteDirectory()
        {
            // absolute path to the directory
            string remotePath = Request[UrlParameter.UrlCommandArgument1];

            // delete the directory
            _sftp.RemoveDirectory(remotePath);

            ShowFileList();
        }

        /// <summary>
        /// Renames the file or directory.
        /// </summary>
        private void Rename()
        {
            // name of the original file or directory
            string originalName = Request[UrlParameter.UrlCommandArgument1];

            // new name of the file or directory
            string newName = Request[UrlParameter.UrlCommandArgument2];

            if (originalName == null)
                originalName = string.Empty;
            else
                originalName.Trim();

            if (newName == null)
                newName = string.Empty;
            else
                newName.Trim();

            if (originalName == string.Empty)
                throw new InvalidOperationException("UrlCommandArgument1 parameter is not filled.");

            if (newName == string.Empty)
                throw new ApplicationException("New name was not specified.");

            // rename the file or directory
            _sftp.Rename(PathCombine(_currentDirectory, originalName), PathCombine(_currentDirectory, newName));

            ShowFileList();
        }

        /// <summary>
        /// Resolves symlink and chooses appropriate action to do 
        /// (download it if it's a file; otherwise list it as a directory).
        /// </summary>
        /// <remarks>
        /// This is a very simple and not accurate heuristic.
        /// </remarks>
        private void ResolveSymlink()
        {
            // absolute path to the symlink
            string remotePath = Request[UrlParameter.UrlCommandArgument1];

            // if the symlink is a regular file, than FileExists method should return true
            if (_sftp.FileExists(remotePath))
                DownloadFile();
            else
                ShowFileList();
        }

        /// <summary>
        /// Redirects to Login page where logout logic is performed.
        /// </summary>
        private void LogOut()
        {
            // redirect to Login page
            Response.Redirect(string.Format("Login.aspx?{0}={1}", UrlParameter.Command, UrlParameterCommand.LogOut));
        }

        #region Helper methods

        /// <summary>
        /// Connects and logs in to the SFTP server. Prepares the toolbar navigation also.
        /// </summary>
        private void ConnectSftp()
        {
            // create new Sftp object
            _sftp = new Sftp();

            // connect to the SFTP server
            _sftp.Connect(_connectionInfo.ServerName, _connectionInfo.Port);

            // log in
            _sftp.Login(_connectionInfo.UserName, _connectionInfo.Password);

            // set the current directory
            if (!string.IsNullOrEmpty(Request[UrlParameter.CurrentFolder]))
                _currentDirectory = Request[UrlParameter.CurrentFolder];
            else
                _currentDirectory = _sftp.GetCurrentDirectory();

            // set current directory label
            ltPath.Text = string.Format("Index of {0}: {1}", _connectionInfo.ServerName, _currentDirectory);

            // set refresh link URL
            hlRefresh.NavigateUrl = AssembleParamterUrl(UrlParameterCommand.List, string.Empty, _currentDirectory);

            // set higher level directory link URL or hide it if it's a root directory
            if (_currentDirectory == Constants.PathDelimiter.ToString())
                liCmdUp.Visible = false;
            else
                hlCmdUp.NavigateUrl = AssembleParamterUrl(
                    UrlParameterCommand.List, string.Empty, GetParentDirectory(_currentDirectory));
        }

        /// <summary>
        /// Combines two SFTP path strings.
        /// </summary>
        /// <param name="path1">First path.</param>
        /// <param name="path2">Second path.</param>
        /// <returns>A string containing the combined paths.</returns>
        protected static string PathCombine(string path1, string path2)
        {
            return string.Format("{0}{1}{2}",
                path1.TrimEnd(Constants.PathDelimiter),
                Constants.PathDelimiter,
                path2.TrimStart(Constants.PathDelimiter));
        }

        /// <summary>
        /// Returns the file name and extension of the specified path string.
        /// </summary>
        /// <param name="path">The path string from which to obtain the file name and extension.</param>
        /// <returns>
        /// A string containing characters after the last directory delimiter character
        /// in path. If the last character of path is a directory delimiter
        /// character, this method returns System.String.Empty. If path is null, this
        /// method returns null.
        /// </returns>
        protected static string GetFileName(string path)
        {
            if (path == null)
                return null;

            int idx = path.LastIndexOf(Constants.PathDelimiter);
            if (idx < 0)
                return path;
            else if (idx == path.Length - 1)
                return string.Empty;
            else
                return path.Substring(idx + 1);
        }

        /// <summary>
        /// Returns the parent directory of the specified path string.
        /// </summary>
        /// <param name="path">The path string from which to obtain the parent directory.</param>
        /// <returns>
        /// A string containing characters before the last directory delimiter character
        /// in path. If a directory delimiter character is the only one character, 
        /// this method returns directory delimiter character. If path is null, this
        /// method returns null.
        /// </returns>
        protected static string GetParentDirectory(string path)
        {
            if (path == null)
                return null;

            int idx = path.LastIndexOf(Constants.PathDelimiter);
            if (idx <= 0)
                return Constants.PathDelimiter.ToString();
            else
                return path.Remove(idx);
        }

        #endregion

        #region URL methods

        /// <summary>
        /// Determines action URL for the current SftpItem.
        /// </summary>
        /// <param name="item">Current SftpItem.</param>
        /// <returns>Action URL for the current SftpItem.</returns>
        protected string GetUrl(SftpItem item)
        {
            if (item == null)
                return string.Empty;

            string path = PathCombine(_currentDirectory, item.Name);

            switch (item.Type)
            {
                case SftpItemType.Directory:
                    return AssembleParamterUrl(UrlParameterCommand.List, string.Empty, path);

                case SftpItemType.RegularFile:
                    return AssembleParamterUrl(UrlParameterCommand.Download, path, _currentDirectory);

                case SftpItemType.Symlink:
                    return AssembleParamterUrl(UrlParameterCommand.Symlink, path, path);

                default:
                    throw new InvalidOperationException(string.Format("Unknown SftpItem '{0}'.", item.Type));
            }
        }

        /// <summary>
        /// Determines a URL for rename operation of the current SftpItem.
        /// </summary>
        /// <param name="item">Current SftpItem.</param>
        /// <returns>URL for rename operation.</returns>
        protected string GetRenameUrl(SftpItem item)
        {
            if (item == null)
                return string.Empty;

            return AssembleParamterUrl(UrlParameterCommand.Rename, item.Name, _currentDirectory);
        }

        /// <summary>
        /// Determines a URL for delete operation of the current SftpItem.
        /// </summary>
        /// <param name="item">Current SftpItem.</param>
        /// <returns>Delete operation URL for the current SftpItem.</returns>
        protected string GetDeleteUrl(SftpItem item)
        {
            if (item == null)
                return string.Empty;

            string path = PathCombine(_currentDirectory, item.Name);

            switch (item.Type)
            {
                case SftpItemType.Directory:
                    return AssembleParamterUrl(UrlParameterCommand.DeleteDirectory, path, _currentDirectory);

                // regular file delete can be used for deleting symlinks
                case SftpItemType.Symlink:
                case SftpItemType.RegularFile:
                    return AssembleParamterUrl(UrlParameterCommand.DeleteFile, path, _currentDirectory);

                default:
                    throw new InvalidOperationException(string.Format("Unknown SftpItem '{0}'.", item.Type));
            }
        }

        /// <summary>
        /// Returns a URL query string.
        /// </summary>
        /// <param name="commandType">Command to perform.</param>
        /// <param name="urlCommandArgument1">First command argument.</param>
        /// <param name="currentDirectory">Current directory path.</param>
        /// <returns>A URL query string.</returns>
        protected static string AssembleParamterUrl(string commandType, string urlCommandArgument1, string currentDirectory)
        {
            return string.Format("?{0}={1}&{2}={3}&{4}={5}",
                UrlParameter.Command, commandType,
                UrlParameter.UrlCommandArgument1, Uri.EscapeDataString(urlCommandArgument1),
                UrlParameter.CurrentFolder, Uri.EscapeDataString(currentDirectory));
        }

        #endregion

        #region Design methods

        /// <summary>
        /// Returns a file size of the current SftpItem as a formated string.
        /// </summary>
        /// <param name="item">Current SftpItem.</param>
        /// <returns>String containing file size.</returns>
        protected static string GetSize(SftpItem item)
        {
            if (item == null || item.IsDirectory)
                return string.Empty;

            if (item.Length < 1024)
                return string.Format("{0} B", item.Length);

            return string.Format("{0:N} KB", item.Length / 1024.0);
        }

        /// <summary>
        /// Determines appropriate image file name from the current item's type.
        /// </summary>
        /// <param name="item">Current SftpItem.</param>
        /// <returns>File name of an image associated with the current item's type.</returns>
        protected static string GetIcon(SftpItem item)
        {
            if (item == null)
                return string.Empty;

            switch (item.Type)
            {
                case SftpItemType.Directory:
                    return GetIcon(IconType.Folder);

                case SftpItemType.RegularFile:
                    return GetIcon(IconType.File);

                case SftpItemType.Symlink:
                    return GetIcon(IconType.Symlink);

                default:
                    throw new InvalidOperationException(string.Format("Unknown SftpItem '{0}'.", item.Type));
            }
        }

        /// <summary>
        /// Determines file name of an image from the IconType.
        /// </summary>
        /// <param name="iconType">An IconType.</param>
        /// <returns>File name of an image.</returns>
        protected static string GetIcon(IconType iconType)
        {
            switch (iconType)
            {
                case IconType.File:
                    return "file.png";
                case IconType.Folder:
                    return "folder.png";
                case IconType.Symlink:
                    return "link.png";
                case IconType.Rename:
                    return "rename.png";
                case IconType.Delete:
                    return "delete.png";
                case IconType.DirectoryUp:
                    return "directory_up.png";
                case IconType.AddFolder:
                    return "create_folder.png";
                case IconType.UploadFile:
                    return "upload_file.png";
                case IconType.Refresh:
                    return "refresh.png";
                default:
                    throw new InvalidOperationException(string.Format("Unknown IconType '{0}'.", iconType));
            }
        }

        #endregion
    }
}