//! ## 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.
 */
use bytesize::ByteSize;
use std::fmt;
use std::time::Instant;

// -- States and progress

/// ### TransferStates
///
/// TransferStates contains the states related to the transfer process
pub struct TransferStates {
    aborted: bool,               // Describes whether the transfer process has been aborted
    pub full: ProgressStates,    // full transfer states
    pub partial: ProgressStates, // Partial transfer states
}

/// ### ProgressStates
///
/// Progress states describes the states for the progress of a single transfer part
pub struct ProgressStates {
    started: Instant,
    total: usize,
    written: usize,
}

impl Default for TransferStates {
    fn default() -> Self {
        Self::new()
    }
}

impl TransferStates {
    /// ### new
    ///
    /// Instantiates a new transfer states
    pub fn new() -> TransferStates {
        TransferStates {
            aborted: false,
            full: ProgressStates::default(),
            partial: ProgressStates::default(),
        }
    }

    /// ### reset
    ///
    /// Re-intiialize transfer states
    pub fn reset(&mut self) {
        self.aborted = false;
    }

    /// ### abort
    ///
    /// Set aborted to true
    pub fn abort(&mut self) {
        self.aborted = true;
    }

    /// ### aborted
    ///
    /// Returns whether transfer has been aborted
    pub fn aborted(&self) -> bool {
        self.aborted
    }

    /// ### full_size
    ///
    /// Returns the size of the entire transfer
    pub fn full_size(&self) -> usize {
        self.full.total
    }
}

impl Default for ProgressStates {
    fn default() -> Self {
        ProgressStates {
            started: Instant::now(),
            written: 0,
            total: 0,
        }
    }
}

impl fmt::Display for ProgressStates {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let eta: String = match self.calc_eta() {
            0 => String::from("--:--"),
            seconds => format!(
                "{:0width$}:{:0width$}",
                (seconds / 60),
                (seconds % 60),
                width = 2
            ),
        };
        write!(
            f,
            "{:.2}% - ETA {} ({}/s)",
            self.calc_progress_percentage(),
            eta,
            ByteSize(self.calc_bytes_per_second())
        )
    }
}

impl ProgressStates {
    /// ### init
    ///
    /// Initialize a new Progress State
    pub fn init(&mut self, sz: usize) {
        self.started = Instant::now();
        self.total = sz;
        self.written = 0;
    }

    /// ### update_progress
    ///
    /// Update progress state
    pub fn update_progress(&mut self, delta: usize) -> f64 {
        self.written += delta;
        self.calc_progress_percentage()
    }

    /// ### calc_progress
    ///
    /// Calculate progress in a range between 0.0 to 1.0
    pub fn calc_progress(&self) -> f64 {
        // Prevent dividing by 0
        if self.total == 0 {
            return 0.0;
        }
        let prog: f64 = (self.written as f64) / (self.total as f64);
        match prog > 1.0 {
            true => 1.0,
            false => prog,
        }
    }

    /// ### started
    ///
    /// Get started
    pub fn started(&self) -> Instant {
        self.started
    }

    /// ### calc_progress_percentage
    ///
    /// Calculate the current transfer progress as percentage
    fn calc_progress_percentage(&self) -> f64 {
        self.calc_progress() * 100.0
    }

    /// ### calc_bytes_per_second
    ///
    /// Generic function to calculate bytes per second using elapsed time since transfer started and the bytes written
    /// and the total amount of bytes to write
    pub fn calc_bytes_per_second(&self) -> u64 {
        // bytes_written : elapsed_secs = x : 1
        let elapsed_secs: u64 = self.started.elapsed().as_secs();
        match elapsed_secs {
            0 => match self.written == self.total {
                // NOTE: would divide by 0 :D
                true => self.total as u64, // Download completed in less than 1 second
                false => 0,                // 0 B/S
            },
            _ => self.written as u64 / elapsed_secs,
        }
    }

    /// ### calc_eta
    ///
    /// Calculate ETA for current transfer as seconds
    fn calc_eta(&self) -> u64 {
        let elapsed_secs: u64 = self.started.elapsed().as_secs();
        let prog: f64 = self.calc_progress_percentage();
        match prog as u64 {
            0 => 0,
            _ => ((elapsed_secs * 100) / (prog as u64)) - elapsed_secs,
        }
    }
}

// -- Options

/// ## TransferOpts
///
/// Defines the transfer options for transfer actions
pub struct TransferOpts {
    /// Save file as
    pub save_as: Option<String>,
    /// Whether to check if file is being replaced
    pub check_replace: bool,
}

impl Default for TransferOpts {
    fn default() -> Self {
        Self {
            save_as: None,
            check_replace: true,
        }
    }
}

impl TransferOpts {
    /// ### save_as
    ///
    /// Define the name of the file to be saved
    pub fn save_as<S: AsRef<str>>(mut self, n: Option<S>) -> Self {
        self.save_as = n.map(|x| x.as_ref().to_string());
        self
    }

    /// ### check_replace
    ///
    /// Set whether to check if the file being transferred will "replace" an existing one
    pub fn check_replace(mut self, opt: bool) -> Self {
        self.check_replace = opt;
        self
    }
}

#[cfg(test)]
mod test {

    use super::*;

    use pretty_assertions::assert_eq;
    use std::time::Duration;

    #[test]
    fn test_ui_activities_filetransfer_lib_transfer_progress_states() {
        let mut states: ProgressStates = ProgressStates::default();
        assert_eq!(states.total, 0);
        assert_eq!(states.written, 0);
        assert!(states.started().elapsed().as_secs() < 5);
        // Init new transfer
        states.init(1024);
        assert_eq!(states.total, 1024);
        assert_eq!(states.written, 0);
        assert_eq!(states.calc_bytes_per_second(), 0);
        assert_eq!(states.calc_eta(), 0);
        assert_eq!(states.calc_progress_percentage(), 0.0);
        assert_eq!(states.calc_progress(), 0.0);
        assert_eq!(states.to_string().as_str(), "0.00% - ETA --:-- (0 B/s)");
        // Wait 4 second (virtually)
        states.started = states.started.checked_sub(Duration::from_secs(4)).unwrap();
        // Update state
        states.update_progress(256);
        assert_eq!(states.total, 1024);
        assert_eq!(states.written, 256);
        assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
        assert_eq!(states.calc_eta(), 12); // 16 total sub 4
        assert_eq!(states.calc_progress_percentage(), 25.0);
        assert_eq!(states.calc_progress(), 0.25);
        assert_eq!(states.to_string().as_str(), "25.00% - ETA 00:12 (64 B/s)");
        // 100%
        states.started = states.started.checked_sub(Duration::from_secs(12)).unwrap();
        states.update_progress(768);
        assert_eq!(states.total, 1024);
        assert_eq!(states.written, 1024);
        assert_eq!(states.calc_bytes_per_second(), 64); // 256 bytes in 4 seconds
        assert_eq!(states.calc_eta(), 0); // 16 total sub 4
        assert_eq!(states.calc_progress_percentage(), 100.0);
        assert_eq!(states.calc_progress(), 1.0);
        assert_eq!(states.to_string().as_str(), "100.00% - ETA --:-- (64 B/s)");
        // Check if terminated at started
        states.started = Instant::now();
        assert_eq!(states.calc_bytes_per_second(), 1024);
        // Divide by zero
        let states: ProgressStates = ProgressStates::default();
        assert_eq!(states.total, 0);
        assert_eq!(states.written, 0);
        assert_eq!(states.calc_progress(), 0.0);
    }

    #[test]
    fn test_ui_activities_filetransfer_lib_transfer_states() {
        let mut states: TransferStates = TransferStates::default();
        assert_eq!(states.aborted, false);
        assert_eq!(states.full.total, 0);
        assert_eq!(states.full.written, 0);
        assert!(states.full.started.elapsed().as_secs() < 5);
        assert_eq!(states.partial.total, 0);
        assert_eq!(states.partial.written, 0);
        assert!(states.partial.started.elapsed().as_secs() < 5);
        // Aborted
        states.abort();
        assert_eq!(states.aborted(), true);
        states.reset();
        assert_eq!(states.aborted(), false);
        states.full.total = 1024;
        assert_eq!(states.full_size(), 1024);
    }

    #[test]
    fn transfer_opts() {
        let opts = TransferOpts::default();
        assert_eq!(opts.check_replace, true);
        assert!(opts.save_as.is_none());
        let opts = TransferOpts::default()
            .check_replace(false)
            .save_as(Some("omar.txt"));
        assert_eq!(opts.save_as.as_deref().unwrap(), "omar.txt");
        assert_eq!(opts.check_replace, false);
    }
}
