use std::fs::File;
use std::io::prelude::*;
use std::path::Path;
use std::time::{Duration, Instant};
use std::vec::Vec;

/// Chrono that tic, toc, and format elasped time
///
/// # Example
///
/// ```
/// use kalast::Chrono;
///
/// let chrono = Chrono::tic();
///
/// // after 54 seconds..
///
/// println!("Time elapsed: {:?}s", chrono.toc());    //
/// println!("Time elapsed: {}", chrono.toc_str()); // all print -> Elapsed time: 54s
/// chrono.toc_str_print();                         //
/// ```
///
/// and it works with seconds, minutes, hours and days.
pub struct Chrono {
    start: Instant,
}

/// # Chrono constructor and usage
impl Chrono {
    /// Chrono constructor.
    pub fn tic() -> Self {
        Self {
            start: Instant::now(),
        }
    }

    /// Get elapsed time.
    pub fn toc(&self) -> Duration {
        self.start.elapsed()
    }

    /// Get elapsed time formatted with seconds, minutes, hours and days.
    pub fn toc_str(&self) -> String {
        let time_names = &["s", "m", "h", "d"];
        let time_divides = &[60., 60., 24.];
        let mut time_names_count = 0;
        let mut elapsed = self.toc().as_millis() as f64 * 1e-3;
        for _ in 0..time_names.len() {
            if elapsed > time_divides[time_names_count] {
                elapsed /= time_divides[time_names_count];
                time_names_count += 1;
            }
        }
        format!(
            "{}{}",
            format_significative_number(elapsed, 3),
            time_names[time_names_count]
        )
    }

    /// Display elapsed time.
    pub fn toc_str_print(&self) {
        println!("Elapsed time: {}", self.toc_str());
    }
}

/// Export data to file with header
///
/// # Example
///
/// ```
/// use std::path::Path;
/// use kalast::ExportFile;
///
/// let mut file = ExportFile::new(Path::new("my_file.txt"));
/// file.add_to_header("Header title".to_string());
/// file.add_to_header("Header contents".to_string());
/// for i in 0..5 {
///     file.add_to_contents(format!("{}", i));
/// }
/// file.write();
///
/// /*
/// Expected output in "my_file.txt":
/// ---
/// Header title
/// Header contents
/// ---
/// 0
/// 1
/// 2
/// 3
/// 4
/// */
/// ```
pub struct ExportFile {
    path: &'static Path,
    header: Vec<String>,
    contents: Vec<String>,
}

/// # File creation and write
impl ExportFile {
    /// Constructor.
    pub fn new(path: &'static Path) -> Self {
        ExportFile {
            path,
            header: vec![],
            contents: vec![],
        }
    }

    /// Add a line to the header (no newline character needed).
    pub fn add_to_header(&mut self, string: String) {
        self.header.push(string);
    }

    /// Add a line to the contents of the file (no newline character needed).
    pub fn add_to_contents(&mut self, string: String) {
        self.contents.push(string);
    }

    /// Write the file.
    pub fn write(&self) {
        let mut file = File::create(self.path.to_str().unwrap()).unwrap();
        self.write_line(&mut file, "---".to_string());
        for line in self.header.iter() {
            self.write_line(&mut file, line.clone());
        }
        self.write_line(&mut file, "---".to_string());
        for line in self.contents.iter() {
            self.write_line(&mut file, line.clone());
        }
    }

    /// Alias to write a single line (add the newline character and convert to bytes)
    fn write_line(&self, file: &mut File, line: String) {
        file.write_all(format!("{}\n", line).as_bytes()).unwrap();
    }
}

/// Display the type of the variable.
pub fn print_type_of<T>(_: &T) {
    println!("{}", std::any::type_name::<T>())
}

/// Display float with significative number precision
fn format_significative_number(float: f64, precision: usize) -> String {
    // compute absolute value
    let a = float.abs();
    // if abs value is greater than 1, then precision becomes less than "standard"
    let precision = if a >= 1. {
        // reduce by number of digits, minimum 0
        let n = (1. + a.log10().floor()) as usize;
        if n <= precision {
            precision - n
        } else {
            0
        }
    // if precision is less than 1 (but non-zero), then precision becomes greater than "standard"
    } else if a > 0. {
        // increase number of digits
        let n = -(1. + a.log10().floor()) as usize;
        precision + n
    // special case for 0
    } else {
        0
    };
    // format with the given computed precision
    format!("{0:.1$}", float, precision)
}
