//! CLI for generation of a CID (v0 and v1) from a given file.
//! Adapted from [this `rust-ipfs` example](https://github.com/rs-ipfs/rust-ipfs/blob/master/unixfs/examples/add.rs)
//! Heavily influenced by [example form the Rust Book](https://doc.rust-lang.org/stable/book/ch12-00-an-io-project.html) for use of parcing inputs and reading a single file.
//! 
//! # Example:
//! ```bash
//! rust_cid_npm test.txt
//! ```

use cid::Cid;
use ipfs_unixfs::file::adder::FileAdder;

use std::error::Error;
use std::fmt;
use std::env;
use std::fs;
use std::process;
use std::time::Duration;


fn main() {

    let config = Config::new(env::args()).unwrap_or_else(|err| {
        eprintln!("Problem parsing arguments: {}", err);
        process::exit(1);
    });

    if let Err(e) = self::run(config) {
        eprintln!("Application error: {}", e);

        process::exit(1);
    }

}

fn run(config: Config) -> Result<String, Box<dyn Error>> {
    
    let mut adder = FileAdder::default();

    let mut stats = Stats::default();
    
    let mut input = 0;

    let contents = fs::read(&config.filename)?;
    
    let start = std::time::Instant::now();

    println!("Reading from file: \"{}\" ...",&config.filename);

    let mut total = 0;

    while total < contents.len() {
        let (blocks, consumed) = adder.push(&contents[total..]);
        stats.process(blocks);

        input += consumed;
        total += consumed;
    }

    let blocks = adder.finish();
    stats.process(blocks);

    assert_eq!(total, contents.len());

    let process_stats = get_process_stats();

    eprintln!("{}", stats);

    let total = start.elapsed();

    if let Some(process_stats) = process_stats {
        eprint!("{}, ", process_stats);
    }

    eprintln!("total: {:?}", total);

    let megabytes = 1024.0 * 1024.0;

    eprintln!(
        "Input: {:.2} MB/s (read {} bytes)",
        (input as f64 / megabytes) / total.as_secs_f64(),
        input
    );

    eprintln!(
        "Output: {:.2} MB/s",
        (stats.block_bytes as f64 / megabytes) / total.as_secs_f64()
    );

    Ok(format!("{}",stats))
}

pub struct Config {
    pub filename: String,
}

impl Config {
    /// Make a new instance

    pub fn new(mut args: env::Args) -> Result<Config, &'static str> {
        //Consume the args given to config by env::args() -- expecting [~/..../rust_cid_npm <FILENAME>]
        args.next(); // skip calling arg/program

        let filename = match args.next() {
            Some(arg) => arg,
            None => return Err("Didn't get a file name.\nExpected to call this executable with path of a file.\n\nExample:\n
            rust_cid_npm text.txt\nExiting..."),
        };

        Ok(Config {
            filename,
        })
    }
}

#[derive(Default)]
struct Stats {
    blocks: usize,
    block_bytes: u64,
    last: Option<Cid>,
}

impl fmt::Display for Stats {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        let cidv0 = self.last.as_ref().unwrap();
        let cidv1 = Cid::new_v1(cid::Codec::DagProtobuf, cidv0.hash().to_owned());
        write!(
            fmt,
            "{{\"CIDv0\":\"{}\",\"CIDv1\":\"{}\"}}",
            cidv0,
            cidv1,
            // self.blocks,
            // self.block_bytes,
        )
    }
}

impl Stats {
    fn process<I: Iterator<Item = (Cid, Vec<u8>)>>(&mut self, new_blocks: I) {
        for (cid, block) in new_blocks {
            self.last = Some(cid);
            self.blocks += 1;
            self.block_bytes += block.len() as u64;
        }
    }
}

struct ProcessStats {
    user_time: Duration,
    system_time: Duration,
    max_rss: i64,
}

impl fmt::Display for ProcessStats {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            fmt,
            "Max RSS: {} KB, utime: {:?}, stime: {:?}",
            self.max_rss, self.user_time, self.system_time
        )
    }
}

#[cfg(unix)]
fn get_process_stats() -> Option<ProcessStats> {
    fn to_duration(tv: libc::timeval) -> Duration {
        assert!(tv.tv_sec >= 0);
        Duration::new(tv.tv_sec as u64, tv.tv_usec as u32)
    }

    let (max_rss, user_time, system_time) = unsafe {
        let mut rusage: libc::rusage = std::mem::zeroed();

        let retval = libc::getrusage(libc::RUSAGE_SELF, &mut rusage as *mut _);

        assert_eq!(retval, 0);

        (rusage.ru_maxrss, rusage.ru_utime, rusage.ru_stime)
    };

    let user_time = to_duration(user_time);
    let system_time = to_duration(system_time);

    Some(ProcessStats {
        user_time,
        system_time,
        max_rss,
    })
}

#[cfg(not(unix))]
fn get_process_stats() -> Option<ProcessStats> {
    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    pub fn test_cid_cli(){
        // Known test case for simple string "abcdefg\n" as a Vec<u8> -- tool that can help: https://onlineunicodetools.com/convert-unicode-to-utf8  -- note that u8 is ecoded in decimal!!! "abcdefg\n" = vec![97, 98, 99, 100, 101, 102, 103, 10]
        // NOTE: the expects a test file "./tests/test.txt" assuming your working directory contains `Cargo.toml`! This file must contain exactly this string without quotes: "abcdefg\n" (\n is newline, unix like, UTF8 = `10` in decimal)

        let test_file = Config {
            filename: String::from("tests/test.txt")
        };

        let cidv0 = "QmYhMRHZAceoM8L21yqBcJjk1TXasZzHw4g1qzTSHBLq1c";
        let cidv1 = "bafybeiez4kcddcze2tyxsrxzp3lbtx6jyue3mhbg4uo2t2mmdfywwp3sxm";

        let expected = String::from(format!("{{\"CIDv0\":\"{}\",\"CIDv1\":\"{}\"}}",cidv0,cidv1));

        let result = run(test_file).unwrap();

        assert_eq!(expected, result);    
    }
}