//! ## FileTransferActivity
//!
//! `filetransfer_activiy` is the module which implements the Filetransfer activity, which is the main activity afterall

/**
 * MIT License
 *
 * termscp - Copyright (c) 2021 Christian Visintin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * 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.
 */
// Locals
use super::{FileTransferActivity, LogLevel};
use crate::filetransfer::{FileTransferError, FileTransferErrorType};
use crate::fs::{FsEntry, FsFile};
use crate::host::HostError;
use crate::utils::fmt::fmt_millis;

// Ext
use bytesize::ByteSize;
use std::fs::File;
use std::io::{Read, Seek, Write};
use std::path::{Path, PathBuf};
use std::time::Instant;
use thiserror::Error;

/// ## TransferErrorReason
///
/// Describes the reason that caused an error during a file transfer
#[derive(Error, Debug)]
enum TransferErrorReason {
    #[error("File transfer aborted")]
    Abrupted,
    #[error("Failed to seek file: {0}")]
    CouldNotRewind(std::io::Error),
    #[error("I/O error on localhost: {0}")]
    LocalIoError(std::io::Error),
    #[error("Host error: {0}")]
    HostError(HostError),
    #[error("I/O error on remote: {0}")]
    RemoteIoError(std::io::Error),
    #[error("File transfer error: {0}")]
    FileTransferError(FileTransferError),
}

/// ## TransferPayload
///
/// Represents the entity to send or receive during a transfer.
/// - File: describes an individual `FsFile` to send
/// - Any: Can be any kind of `FsEntry`, but just one
/// - Many: a list of `FsEntry`
#[derive(Debug)]
pub(super) enum TransferPayload {
    File(FsFile),
    Any(FsEntry),
    Many(Vec<FsEntry>),
}

impl FileTransferActivity {
    /// ### connect
    ///
    /// Connect to remote
    pub(super) fn connect(&mut self) {
        let ft_params = self.context().ft_params().unwrap().clone();
        let entry_dir: Option<PathBuf> = ft_params.entry_directory.clone();
        // Connect to remote
        match self.client.connect(&ft_params.params) {
            Ok(welcome) => {
                if let Some(banner) = welcome {
                    // Log welcome
                    self.log(
                        LogLevel::Info,
                        format!(
                            "Established connection with '{}': \"{}\"",
                            self.get_remote_hostname(),
                            banner
                        ),
                    );
                }
                // Try to change directory to entry directory
                let mut remote_chdir: Option<PathBuf> = None;
                if let Some(entry_directory) = &entry_dir {
                    remote_chdir = Some(entry_directory.clone());
                }
                if let Some(entry_directory) = remote_chdir {
                    self.remote_changedir(entry_directory.as_path(), false);
                }
                // Set state to explorer
                self.umount_wait();
                self.reload_remote_dir();
                // Update file lists
                self.update_local_filelist();
                self.update_remote_filelist();
            }
            Err(err) => {
                // Set popup fatal error
                self.umount_wait();
                self.mount_fatal(&err.to_string());
            }
        }
    }

    /// ### disconnect
    ///
    /// disconnect from remote
    pub(super) fn disconnect(&mut self) {
        let msg: String = format!("Disconnecting from {}…", self.get_remote_hostname());
        // Show popup disconnecting
        self.mount_wait(msg.as_str());
        // Disconnect
        let _ = self.client.disconnect();
        // Quit
        self.exit_reason = Some(super::ExitReason::Disconnect);
    }

    /// ### disconnect_and_quit
    ///
    /// disconnect from remote and then quit
    pub(super) fn disconnect_and_quit(&mut self) {
        self.disconnect();
        self.exit_reason = Some(super::ExitReason::Quit);
    }

    /// ### reload_remote_dir
    ///
    /// Reload remote directory entries and update browser
    pub(super) fn reload_remote_dir(&mut self) {
        // Get current entries
        if let Ok(wrkdir) = self.client.pwd() {
            self.remote_scan(wrkdir.as_path());
            // Set wrkdir
            self.remote_mut().wrkdir = wrkdir;
        }
    }

    /// ### reload_local_dir
    ///
    /// Reload local directory entries and update browser
    pub(super) fn reload_local_dir(&mut self) {
        let wrkdir: PathBuf = self.host.pwd();
        self.local_scan(wrkdir.as_path());
        self.local_mut().wrkdir = wrkdir;
    }

    /// ### local_scan
    ///
    /// Scan current local directory
    fn local_scan(&mut self, path: &Path) {
        match self.host.scan_dir(path) {
            Ok(files) => {
                // Set files and sort (sorting is implicit)
                self.local_mut().set_files(files);
            }
            Err(err) => {
                self.log_and_alert(
                    LogLevel::Error,
                    format!("Could not scan current directory: {}", err),
                );
            }
        }
    }

    /// ### remote_scan
    ///
    /// Scan current remote directory
    fn remote_scan(&mut self, path: &Path) {
        match self.client.list_dir(path) {
            Ok(files) => {
                // Set files and sort (sorting is implicit)
                self.remote_mut().set_files(files);
            }
            Err(err) => {
                self.log_and_alert(
                    LogLevel::Error,
                    format!("Could not scan current directory: {}", err),
                );
            }
        }
    }

    /// ### filetransfer_send
    ///
    /// Send fs entry to remote.
    /// If dst_name is Some, entry will be saved with a different name.
    /// If entry is a directory, this applies to directory only
    pub(super) fn filetransfer_send(
        &mut self,
        payload: TransferPayload,
        curr_remote_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Use different method based on payload
        let result = match payload {
            TransferPayload::Any(ref entry) => {
                self.filetransfer_send_any(entry, curr_remote_path, dst_name)
            }
            TransferPayload::File(ref file) => {
                self.filetransfer_send_file(file, curr_remote_path, dst_name)
            }
            TransferPayload::Many(ref entries) => {
                self.filetransfer_send_many(entries, curr_remote_path)
            }
        };
        // Notify
        match &result {
            Ok(_) => {
                self.notify_transfer_completed(&payload);
            }
            Err(e) => {
                self.notify_transfer_error(e.as_str());
            }
        }
        result
    }

    /// ### filetransfer_send_file
    ///
    /// Send one file to remote at specified path.
    fn filetransfer_send_file(
        &mut self,
        file: &FsFile,
        curr_remote_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total size of transfer
        let total_transfer_size: usize = file.size;
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Uploading {}…", file.abs_path.display()));
        // Get remote path
        let file_name: String = file.name.clone();
        let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
        let remote_file_name: PathBuf = match dst_name {
            Some(s) => PathBuf::from(s.as_str()),
            None => PathBuf::from(file_name.as_str()),
        };
        remote_path.push(remote_file_name);
        // Send
        let result = self.filetransfer_send_one(file, remote_path.as_path(), file_name);
        // Umount progress bar
        self.umount_progress_bar();
        // Return result
        result.map_err(|x| x.to_string())
    }

    /// ### filetransfer_send_any
    ///
    /// Send a `TransferPayload` of type `Any`
    fn filetransfer_send_any(
        &mut self,
        entry: &FsEntry,
        curr_remote_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total size of transfer
        let total_transfer_size: usize = self.get_total_transfer_size_local(entry);
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Uploading {}…", entry.get_abs_path().display()));
        // Send recurse
        let result = self.filetransfer_send_recurse(entry, curr_remote_path, dst_name);
        // Umount progress bar
        self.umount_progress_bar();
        result
    }

    /// ### filetransfer_send_many
    ///
    /// Send many entries to remote
    fn filetransfer_send_many(
        &mut self,
        entries: &[FsEntry],
        curr_remote_path: &Path,
    ) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total size of transfer
        let total_transfer_size: usize = entries
            .iter()
            .map(|x| self.get_total_transfer_size_local(x))
            .sum();
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Uploading {} entries…", entries.len()));
        // Send recurse
        let result = entries
            .iter()
            .map(|x| self.filetransfer_send_recurse(x, curr_remote_path, None))
            .find(|x| x.is_err())
            .unwrap_or(Ok(()));
        // Umount progress bar
        self.umount_progress_bar();
        result
    }

    fn filetransfer_send_recurse(
        &mut self,
        entry: &FsEntry,
        curr_remote_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Write popup
        let file_name: String = match entry {
            FsEntry::Directory(dir) => dir.name.clone(),
            FsEntry::File(file) => file.name.clone(),
        };
        // Get remote path
        let mut remote_path: PathBuf = PathBuf::from(curr_remote_path);
        let remote_file_name: PathBuf = match dst_name {
            Some(s) => PathBuf::from(s.as_str()),
            None => PathBuf::from(file_name.as_str()),
        };
        remote_path.push(remote_file_name);
        // Match entry
        let result: Result<(), String> = match entry {
            FsEntry::File(file) => {
                match self.filetransfer_send_one(file, remote_path.as_path(), file_name) {
                    Err(err) => {
                        // If transfer was abrupted or there was an IO error on remote, remove file
                        if matches!(
                            err,
                            TransferErrorReason::Abrupted | TransferErrorReason::RemoteIoError(_)
                        ) {
                            // Stat file on remote and remove it if exists
                            match self.client.stat(remote_path.as_path()) {
                                Err(err) => self.log(
                                    LogLevel::Error,
                                    format!(
                                        "Could not remove created file {}: {}",
                                        remote_path.display(),
                                        err
                                    ),
                                ),
                                Ok(entry) => {
                                    if let Err(err) = self.client.remove(&entry) {
                                        self.log(
                                            LogLevel::Error,
                                            format!(
                                                "Could not remove created file {}: {}",
                                                remote_path.display(),
                                                err
                                            ),
                                        );
                                    }
                                }
                            }
                        }
                        Err(err.to_string())
                    }
                    Ok(_) => Ok(()),
                }
            }
            FsEntry::Directory(dir) => {
                // Create directory on remote first
                match self.client.mkdir(remote_path.as_path()) {
                    Ok(_) => {
                        self.log(
                            LogLevel::Info,
                            format!("Created directory \"{}\"", remote_path.display()),
                        );
                    }
                    Err(err) if err.kind() == FileTransferErrorType::DirectoryAlreadyExists => {
                        self.log(
                            LogLevel::Info,
                            format!(
                                "Directory \"{}\" already exists on remote",
                                remote_path.display()
                            ),
                        );
                    }
                    Err(err) => {
                        self.log_and_alert(
                            LogLevel::Error,
                            format!(
                                "Failed to create directory \"{}\": {}",
                                remote_path.display(),
                                err
                            ),
                        );
                        return Err(err.to_string());
                    }
                }
                // Get files in dir
                match self.host.scan_dir(dir.abs_path.as_path()) {
                    Ok(entries) => {
                        // Iterate over files
                        for entry in entries.iter() {
                            // If aborted; break
                            if self.transfer.aborted() {
                                break;
                            }
                            // Send entry; name is always None after first call
                            if let Err(err) =
                                self.filetransfer_send_recurse(entry, remote_path.as_path(), None)
                            {
                                return Err(err);
                            }
                        }
                        Ok(())
                    }
                    Err(err) => {
                        self.log_and_alert(
                            LogLevel::Error,
                            format!(
                                "Could not scan directory \"{}\": {}",
                                dir.abs_path.display(),
                                err
                            ),
                        );
                        Err(err.to_string())
                    }
                }
            }
        };
        // Scan dir on remote
        self.reload_remote_dir();
        // If aborted; show popup
        if self.transfer.aborted() {
            // Log abort
            self.log_and_alert(
                LogLevel::Warn,
                format!("Upload aborted for \"{}\"!", entry.get_abs_path().display()),
            );
        }
        result
    }

    /// ### filetransfer_send_file
    ///
    /// Send local file and write it to remote path
    fn filetransfer_send_one(
        &mut self,
        local: &FsFile,
        remote: &Path,
        file_name: String,
    ) -> Result<(), TransferErrorReason> {
        // Upload file
        // Try to open local file
        match self.host.open_file_read(local.abs_path.as_path()) {
            Ok(fhnd) => match self.client.send_file(local, remote) {
                Ok(rhnd) => {
                    self.filetransfer_send_one_with_stream(local, remote, file_name, fhnd, rhnd)
                }
                Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
                    self.filetransfer_send_one_wno_stream(local, remote, file_name, fhnd)
                }
                Err(err) => Err(TransferErrorReason::FileTransferError(err)),
            },
            Err(err) => Err(TransferErrorReason::HostError(err)),
        }
    }

    /// ### filetransfer_send_one_with_stream
    ///
    /// Send file to remote using stream
    fn filetransfer_send_one_with_stream(
        &mut self,
        local: &FsFile,
        remote: &Path,
        file_name: String,
        mut reader: File,
        mut writer: Box<dyn Write>,
    ) -> Result<(), TransferErrorReason> {
        // Write file
        let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
        // Init transfer
        self.transfer.partial.init(file_size);
        // rewind
        if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
            return Err(TransferErrorReason::CouldNotRewind(err));
        }
        // Write remote file
        let mut total_bytes_written: usize = 0;
        let mut last_progress_val: f64 = 0.0;
        let mut last_input_event_fetch: Option<Instant> = None;
        // While the entire file hasn't been completely written,
        // Or filetransfer has been aborted
        while total_bytes_written < file_size && !self.transfer.aborted() {
            // Handle input events (each 500ms) or if never fetched before
            if last_input_event_fetch.is_none()
                || last_input_event_fetch
                    .unwrap_or_else(Instant::now)
                    .elapsed()
                    .as_millis()
                    >= 500
            {
                // Read events
                self.read_input_event();
                // Reset instant
                last_input_event_fetch = Some(Instant::now());
            }
            // Read till you can
            let mut buffer: [u8; 65536] = [0; 65536];
            let delta: usize = match reader.read(&mut buffer) {
                Ok(bytes_read) => {
                    total_bytes_written += bytes_read;
                    if bytes_read == 0 {
                        continue;
                    } else {
                        let mut delta: usize = 0;
                        while delta < bytes_read {
                            // Write bytes
                            match writer.write(&buffer[delta..bytes_read]) {
                                Ok(bytes) => {
                                    delta += bytes;
                                }
                                Err(err) => {
                                    return Err(TransferErrorReason::RemoteIoError(err));
                                }
                            }
                        }
                        delta
                    }
                }
                Err(err) => {
                    return Err(TransferErrorReason::LocalIoError(err));
                }
            };
            // Increase progress
            self.transfer.partial.update_progress(delta);
            self.transfer.full.update_progress(delta);
            // Draw only if a significant progress has been made (performance improvement)
            if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
                // Draw
                self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
                self.view();
                last_progress_val = self.transfer.partial.calc_progress();
            }
        }
        // Finalize stream
        if let Err(err) = self.client.on_sent(writer) {
            self.log(
                LogLevel::Warn,
                format!("Could not finalize remote stream: \"{}\"", err),
            );
        }
        // if upload was abrupted, return error
        if self.transfer.aborted() {
            return Err(TransferErrorReason::Abrupted);
        }
        self.log(
            LogLevel::Info,
            format!(
                "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
                local.abs_path.display(),
                remote.display(),
                fmt_millis(self.transfer.partial.started().elapsed()),
                ByteSize(self.transfer.partial.calc_bytes_per_second()),
            ),
        );
        Ok(())
    }

    /// ### filetransfer_send_one_wno_stream
    ///
    /// Send an `FsFile` to remote without using streams.
    fn filetransfer_send_one_wno_stream(
        &mut self,
        local: &FsFile,
        remote: &Path,
        file_name: String,
        mut reader: File,
    ) -> Result<(), TransferErrorReason> {
        // Write file
        let file_size: usize = reader.seek(std::io::SeekFrom::End(0)).unwrap_or(0) as usize;
        // Init transfer
        self.transfer.partial.init(file_size);
        // rewind
        if let Err(err) = reader.seek(std::io::SeekFrom::Start(0)) {
            return Err(TransferErrorReason::CouldNotRewind(err));
        }
        // Draw before
        self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
        self.view();
        // Send file
        if let Err(err) = self
            .client
            .send_file_wno_stream(local, remote, Box::new(reader))
        {
            return Err(TransferErrorReason::FileTransferError(err));
        }
        // Set transfer size ok
        self.transfer.partial.update_progress(file_size);
        self.transfer.full.update_progress(file_size);
        // Draw again after
        self.update_progress_bar(format!("Uploading \"{}\"…", file_name));
        self.view();
        // log and return Ok
        self.log(
            LogLevel::Info,
            format!(
                "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
                local.abs_path.display(),
                remote.display(),
                fmt_millis(self.transfer.partial.started().elapsed()),
                ByteSize(self.transfer.partial.calc_bytes_per_second()),
            ),
        );
        Ok(())
    }

    /// ### filetransfer_recv
    ///
    /// Recv fs entry from remote.
    /// If dst_name is Some, entry will be saved with a different name.
    /// If entry is a directory, this applies to directory only
    pub(super) fn filetransfer_recv(
        &mut self,
        payload: TransferPayload,
        local_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        let result = match payload {
            TransferPayload::Any(ref entry) => {
                self.filetransfer_recv_any(entry, local_path, dst_name)
            }
            TransferPayload::File(ref file) => self.filetransfer_recv_file(file, local_path),
            TransferPayload::Many(ref entries) => self.filetransfer_recv_many(entries, local_path),
        };
        // Notify
        match &result {
            Ok(_) => {
                self.notify_transfer_completed(&payload);
            }
            Err(e) => {
                self.notify_transfer_error(e.as_str());
            }
        }
        result
    }

    /// ### filetransfer_recv_any
    ///
    /// Recv fs entry from remote.
    /// If dst_name is Some, entry will be saved with a different name.
    /// If entry is a directory, this applies to directory only
    fn filetransfer_recv_any(
        &mut self,
        entry: &FsEntry,
        local_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total transfer size
        let total_transfer_size: usize = self.get_total_transfer_size_remote(entry);
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Downloading {}…", entry.get_abs_path().display()));
        // Receive
        let result = self.filetransfer_recv_recurse(entry, local_path, dst_name);
        // Umount progress bar
        self.umount_progress_bar();
        result
    }

    /// ### filetransfer_recv_file
    ///
    /// Receive a single file from remote.
    fn filetransfer_recv_file(&mut self, entry: &FsFile, local_path: &Path) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total transfer size
        let total_transfer_size: usize = entry.size;
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Downloading {}…", entry.abs_path.display()));
        // Receive
        let result = self.filetransfer_recv_one(local_path, entry, entry.name.clone());
        // Umount progress bar
        self.umount_progress_bar();
        // Return result
        result.map_err(|x| x.to_string())
    }

    /// ### filetransfer_send_many
    ///
    /// Send many entries to remote
    fn filetransfer_recv_many(
        &mut self,
        entries: &[FsEntry],
        curr_remote_path: &Path,
    ) -> Result<(), String> {
        // Reset states
        self.transfer.reset();
        // Calculate total size of transfer
        let total_transfer_size: usize = entries
            .iter()
            .map(|x| self.get_total_transfer_size_remote(x))
            .sum();
        self.transfer.full.init(total_transfer_size);
        // Mount progress bar
        self.mount_progress_bar(format!("Downloading {} entries…", entries.len()));
        // Send recurse
        let result = entries
            .iter()
            .map(|x| self.filetransfer_recv_recurse(x, curr_remote_path, None))
            .find(|x| x.is_err())
            .unwrap_or(Ok(()));
        // Umount progress bar
        self.umount_progress_bar();
        result
    }

    fn filetransfer_recv_recurse(
        &mut self,
        entry: &FsEntry,
        local_path: &Path,
        dst_name: Option<String>,
    ) -> Result<(), String> {
        // Write popup
        let file_name: String = match entry {
            FsEntry::Directory(dir) => dir.name.clone(),
            FsEntry::File(file) => file.name.clone(),
        };
        // Match entry
        let result: Result<(), String> = match entry {
            FsEntry::File(file) => {
                // Get local file
                let mut local_file_path: PathBuf = PathBuf::from(local_path);
                let local_file_name: String = match dst_name {
                    Some(n) => n,
                    None => file.name.clone(),
                };
                local_file_path.push(local_file_name.as_str());
                // Download file
                if let Err(err) =
                    self.filetransfer_recv_one(local_file_path.as_path(), file, file_name)
                {
                    // If transfer was abrupted or there was an IO error on remote, remove file
                    if matches!(
                        err,
                        TransferErrorReason::Abrupted | TransferErrorReason::LocalIoError(_)
                    ) {
                        // Stat file
                        match self.host.stat(local_file_path.as_path()) {
                            Err(err) => self.log(
                                LogLevel::Error,
                                format!(
                                    "Could not remove created file {}: {}",
                                    local_file_path.display(),
                                    err
                                ),
                            ),
                            Ok(entry) => {
                                if let Err(err) = self.host.remove(&entry) {
                                    self.log(
                                        LogLevel::Error,
                                        format!(
                                            "Could not remove created file {}: {}",
                                            local_file_path.display(),
                                            err
                                        ),
                                    );
                                }
                            }
                        }
                    }
                    Err(err.to_string())
                } else {
                    Ok(())
                }
            }
            FsEntry::Directory(dir) => {
                // Get dir name
                let mut local_dir_path: PathBuf = PathBuf::from(local_path);
                match dst_name {
                    Some(name) => local_dir_path.push(name),
                    None => local_dir_path.push(dir.name.as_str()),
                }
                // Create directory on local
                match self.host.mkdir_ex(local_dir_path.as_path(), true) {
                    Ok(_) => {
                        // Apply file mode to directory
                        #[cfg(any(
                            target_family = "unix",
                            target_os = "macos",
                            target_os = "linux"
                        ))]
                        if let Some((owner, group, others)) = dir.unix_pex {
                            if let Err(err) = self.host.chmod(
                                local_dir_path.as_path(),
                                (owner.as_byte(), group.as_byte(), others.as_byte()),
                            ) {
                                self.log(
                                    LogLevel::Error,
                                    format!(
                                        "Could not apply file mode {:?} to \"{}\": {}",
                                        (owner.as_byte(), group.as_byte(), others.as_byte()),
                                        local_dir_path.display(),
                                        err
                                    ),
                                );
                            }
                        }
                        self.log(
                            LogLevel::Info,
                            format!("Created directory \"{}\"", local_dir_path.display()),
                        );
                        // Get files in dir
                        match self.client.list_dir(dir.abs_path.as_path()) {
                            Ok(entries) => {
                                // Iterate over files
                                for entry in entries.iter() {
                                    // If transfer has been aborted; break
                                    if self.transfer.aborted() {
                                        break;
                                    }
                                    // Receive entry; name is always None after first call
                                    // Local path becomes local_dir_path
                                    if let Err(err) = self.filetransfer_recv_recurse(
                                        entry,
                                        local_dir_path.as_path(),
                                        None,
                                    ) {
                                        return Err(err);
                                    }
                                }
                                Ok(())
                            }
                            Err(err) => {
                                self.log_and_alert(
                                    LogLevel::Error,
                                    format!(
                                        "Could not scan directory \"{}\": {}",
                                        dir.abs_path.display(),
                                        err
                                    ),
                                );
                                Err(err.to_string())
                            }
                        }
                    }
                    Err(err) => {
                        self.log(
                            LogLevel::Error,
                            format!(
                                "Failed to create directory \"{}\": {}",
                                local_dir_path.display(),
                                err
                            ),
                        );
                        Err(err.to_string())
                    }
                }
            }
        };
        // Reload directory on local
        self.reload_local_dir();
        // if aborted; show alert
        if self.transfer.aborted() {
            // Log abort
            self.log_and_alert(
                LogLevel::Warn,
                format!(
                    "Download aborted for \"{}\"!",
                    entry.get_abs_path().display()
                ),
            );
        }
        result
    }

    /// ### filetransfer_recv_one
    ///
    /// Receive file from remote and write it to local path
    fn filetransfer_recv_one(
        &mut self,
        local: &Path,
        remote: &FsFile,
        file_name: String,
    ) -> Result<(), TransferErrorReason> {
        // Try to open local file
        match self.host.open_file_write(local) {
            Ok(local_file) => {
                // Download file from remote
                match self.client.recv_file(remote) {
                    Ok(rhnd) => self.filetransfer_recv_one_with_stream(
                        local, remote, file_name, rhnd, local_file,
                    ),
                    Err(err) if err.kind() == FileTransferErrorType::UnsupportedFeature => {
                        self.filetransfer_recv_one_wno_stream(local, remote, file_name)
                    }
                    Err(err) => Err(TransferErrorReason::FileTransferError(err)),
                }
            }
            Err(err) => Err(TransferErrorReason::HostError(err)),
        }
    }

    /// ### filetransfer_recv_one_with_stream
    ///
    /// Receive an `FsEntry` from remote using stream
    fn filetransfer_recv_one_with_stream(
        &mut self,
        local: &Path,
        remote: &FsFile,
        file_name: String,
        mut reader: Box<dyn Read>,
        mut writer: File,
    ) -> Result<(), TransferErrorReason> {
        let mut total_bytes_written: usize = 0;
        // Init transfer
        self.transfer.partial.init(remote.size);
        // Write local file
        let mut last_progress_val: f64 = 0.0;
        let mut last_input_event_fetch: Option<Instant> = None;
        // While the entire file hasn't been completely read,
        // Or filetransfer has been aborted
        while total_bytes_written < remote.size && !self.transfer.aborted() {
            // Handle input events (each 500 ms) or is None
            if last_input_event_fetch.is_none()
                || last_input_event_fetch
                    .unwrap_or_else(Instant::now)
                    .elapsed()
                    .as_millis()
                    >= 500
            {
                // Read events
                self.read_input_event();
                // Reset instant
                last_input_event_fetch = Some(Instant::now());
            }
            // Read till you can
            let mut buffer: [u8; 65536] = [0; 65536];
            let delta: usize = match reader.read(&mut buffer) {
                Ok(bytes_read) => {
                    total_bytes_written += bytes_read;
                    if bytes_read == 0 {
                        continue;
                    } else {
                        let mut delta: usize = 0;
                        while delta < bytes_read {
                            // Write bytes
                            match writer.write(&buffer[delta..bytes_read]) {
                                Ok(bytes) => delta += bytes,
                                Err(err) => {
                                    return Err(TransferErrorReason::LocalIoError(err));
                                }
                            }
                        }
                        delta
                    }
                }
                Err(err) => {
                    return Err(TransferErrorReason::RemoteIoError(err));
                }
            };
            // Set progress
            self.transfer.partial.update_progress(delta);
            self.transfer.full.update_progress(delta);
            // Draw only if a significant progress has been made (performance improvement)
            if last_progress_val < self.transfer.partial.calc_progress() - 0.01 {
                // Draw
                self.update_progress_bar(format!("Downloading \"{}\"", file_name));
                self.view();
                last_progress_val = self.transfer.partial.calc_progress();
            }
        }
        // Finalize stream
        if let Err(err) = self.client.on_recv(reader) {
            self.log(
                LogLevel::Warn,
                format!("Could not finalize remote stream: \"{}\"", err),
            );
        }
        // If download was abrupted, return Error
        if self.transfer.aborted() {
            return Err(TransferErrorReason::Abrupted);
        }
        // Apply file mode to file
        #[cfg(target_family = "unix")]
        if let Some((owner, group, others)) = remote.unix_pex {
            if let Err(err) = self
                .host
                .chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
            {
                self.log(
                    LogLevel::Error,
                    format!(
                        "Could not apply file mode {:?} to \"{}\": {}",
                        (owner.as_byte(), group.as_byte(), others.as_byte()),
                        local.display(),
                        err
                    ),
                );
            }
        }
        // Log
        self.log(
            LogLevel::Info,
            format!(
                "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
                remote.abs_path.display(),
                local.display(),
                fmt_millis(self.transfer.partial.started().elapsed()),
                ByteSize(self.transfer.partial.calc_bytes_per_second()),
            ),
        );
        Ok(())
    }

    /// ### filetransfer_recv_one_with_stream
    ///
    /// Receive an `FsEntry` from remote without using stream
    fn filetransfer_recv_one_wno_stream(
        &mut self,
        local: &Path,
        remote: &FsFile,
        file_name: String,
    ) -> Result<(), TransferErrorReason> {
        // Init transfer
        self.transfer.partial.init(remote.size);
        // Draw before transfer
        self.update_progress_bar(format!("Downloading \"{}\"", file_name));
        self.view();
        // recv wno stream
        if let Err(err) = self.client.recv_file_wno_stream(remote, local) {
            return Err(TransferErrorReason::FileTransferError(err));
        }
        // Update progress at the end
        self.transfer.partial.update_progress(remote.size);
        self.transfer.full.update_progress(remote.size);
        // Draw after transfer
        self.update_progress_bar(format!("Downloading \"{}\"", file_name));
        self.view();
        // Apply file mode to file
        #[cfg(target_family = "unix")]
        if let Some((owner, group, others)) = remote.unix_pex {
            if let Err(err) = self
                .host
                .chmod(local, (owner.as_byte(), group.as_byte(), others.as_byte()))
            {
                self.log(
                    LogLevel::Error,
                    format!(
                        "Could not apply file mode {:?} to \"{}\": {}",
                        (owner.as_byte(), group.as_byte(), others.as_byte()),
                        local.display(),
                        err
                    ),
                );
            }
        }
        // Log
        self.log(
            LogLevel::Info,
            format!(
                "Saved file \"{}\" to \"{}\" (took {} seconds; at {}/s)",
                remote.abs_path.display(),
                local.display(),
                fmt_millis(self.transfer.partial.started().elapsed()),
                ByteSize(self.transfer.partial.calc_bytes_per_second()),
            ),
        );
        Ok(())
    }

    /// ### local_changedir
    ///
    /// Change directory for local
    pub(super) fn local_changedir(&mut self, path: &Path, push: bool) {
        // Get current directory
        let prev_dir: PathBuf = self.local().wrkdir.clone();
        // Change directory
        match self.host.change_wrkdir(path) {
            Ok(_) => {
                self.log(
                    LogLevel::Info,
                    format!("Changed directory on local: {}", path.display()),
                );
                // Reload files
                self.reload_local_dir();
                // Push prev_dir to stack
                if push {
                    self.local_mut().pushd(prev_dir.as_path())
                }
            }
            Err(err) => {
                // Report err
                self.log_and_alert(
                    LogLevel::Error,
                    format!("Could not change working directory: {}", err),
                );
            }
        }
    }

    pub(super) fn remote_changedir(&mut self, path: &Path, push: bool) {
        // Get current directory
        let prev_dir: PathBuf = self.remote().wrkdir.clone();
        // Change directory
        match self.client.as_mut().change_dir(path) {
            Ok(_) => {
                self.log(
                    LogLevel::Info,
                    format!("Changed directory on remote: {}", path.display()),
                );
                // Update files
                self.reload_remote_dir();
                // Push prev_dir to stack
                if push {
                    self.remote_mut().pushd(prev_dir.as_path())
                }
            }
            Err(err) => {
                // Report err
                self.log_and_alert(
                    LogLevel::Error,
                    format!("Could not change working directory: {}", err),
                );
            }
        }
    }

    /// ### download_file_as_temp
    ///
    /// Download provided file as a temporary file
    pub(super) fn download_file_as_temp(&mut self, file: &FsFile) -> Result<PathBuf, String> {
        let tmpfile: PathBuf = match self.cache.as_ref() {
            Some(cache) => {
                let mut p: PathBuf = cache.path().to_path_buf();
                p.push(file.name.as_str());
                p
            }
            None => {
                return Err(String::from(
                    "Could not create tempfile: cache not available",
                ))
            }
        };
        // Download file
        match self.filetransfer_recv(
            TransferPayload::File(file.clone()),
            tmpfile.as_path(),
            Some(file.name.clone()),
        ) {
            Err(err) => Err(format!(
                "Could not download {} to temporary file: {}",
                file.abs_path.display(),
                err
            )),
            Ok(()) => Ok(tmpfile),
        }
    }

    // -- transfer sizes

    /// ### get_total_transfer_size_local
    ///
    /// Get total size of transfer for localhost
    fn get_total_transfer_size_local(&mut self, entry: &FsEntry) -> usize {
        match entry {
            FsEntry::File(file) => file.size,
            FsEntry::Directory(dir) => {
                // List dir
                match self.host.scan_dir(dir.abs_path.as_path()) {
                    Ok(files) => files
                        .iter()
                        .map(|x| self.get_total_transfer_size_local(x))
                        .sum(),
                    Err(err) => {
                        self.log(
                            LogLevel::Error,
                            format!(
                                "Could not list directory {}: {}",
                                dir.abs_path.display(),
                                err
                            ),
                        );
                        0
                    }
                }
            }
        }
    }

    /// ### get_total_transfer_size_remote
    ///
    /// Get total size of transfer for remote host
    fn get_total_transfer_size_remote(&mut self, entry: &FsEntry) -> usize {
        match entry {
            FsEntry::File(file) => file.size,
            FsEntry::Directory(dir) => {
                // List directory
                match self.client.list_dir(dir.abs_path.as_path()) {
                    Ok(files) => files
                        .iter()
                        .map(|x| self.get_total_transfer_size_remote(x))
                        .sum(),
                    Err(err) => {
                        self.log(
                            LogLevel::Error,
                            format!(
                                "Could not list directory {}: {}",
                                dir.abs_path.display(),
                                err
                            ),
                        );
                        0
                    }
                }
            }
        }
    }

    // -- file exist

    pub(crate) fn local_file_exists(&mut self, p: &Path) -> bool {
        self.host.file_exists(p)
    }

    pub(crate) fn remote_file_exists(&mut self, p: &Path) -> bool {
        self.client.stat(p).is_ok()
    }
}
