use std::collections::BTreeMap;
use std::fs::File;
use std::io::{prelude::*, BufReader};
use std::thread;

struct Device {
    // TODO: this should be a structure of counters, perhaps holding all the proc data
    name: String,

    ibytes: i64,
    obytes: i64,
    ipackets: i64,
    opackets: i64,
}

struct DeviceHist {
    history: Vec<Device>,
}

enum ProcNetDevIndexes {
    Ibytes = 1,
    Ipackets = 2,
    Obytes = 9,
    Opackets = 10,
}

fn line_to_device_struct(line: &str) -> Result<Device, ()> {
    let idx = line.find(':');
    if idx == None {
        return Err(());
    }

    let part_split = line[idx.unwrap()..].split_ascii_whitespace();
    let vec_list = part_split.collect::<Vec<&str>>();

    let d = Device {
        name: (line[0..idx.unwrap()].trim()).to_string(),
        ibytes: vec_list[ProcNetDevIndexes::Ibytes as usize]
            .parse()
            .unwrap_or(0),
        obytes: vec_list[ProcNetDevIndexes::Obytes as usize]
            .parse()
            .unwrap_or(0),
        ipackets: vec_list[ProcNetDevIndexes::Ipackets as usize]
            .parse()
            .unwrap_or(0),
        opackets: vec_list[ProcNetDevIndexes::Opackets as usize]
            .parse()
            .unwrap_or(0),
    };
    Ok(d)
}

fn read_proc_net() -> Vec<Device> {
    let mut devs = Vec::new();
    let file = File::open("/proc/net/dev").unwrap();
    let reader = BufReader::new(file);
    for (_, line) in reader.lines().enumerate() {
        let a = line.unwrap();
        let d = line_to_device_struct(&a);
        match d {
            Err(_x) => {}
            Ok(d) => {
                devs.push(d);
            }
        }
    }

    devs
}

fn human_unit(value: i64) -> String {
    let kb = 1024;
    let mb = 1024 * 1024;
    let gb = 1024 * 1024 * 1024;
    let tb = 1024 * 1024 * 1024 * 1024;
    let pb = 1024 * 1024 * 1024 * 1024 * 1024;
    if value >= pb {
        return format!("{} PiB", value / pb);
    }
    if value >= tb {
        return format!("{} TiB", value / tb);
    }
    if value >= gb {
        return format!("{} GiB", value / gb);
    }
    if value >= mb {
        return format!("{} MiB", value / mb);
    }
    if value >= kb {
        return format!("{} KiB", value / kb);
    }
    value.to_string()
}

fn print_line_stats(
    i: &str,
    in_total: i64,
    out_total: i64,
    isec: i64,
    osec: i64,
    imin: i64,
    omin: i64,
) {
    println!(
        "{:17} {:8} {:8} | {:8} | {:8} | {:8} | {:8}",
        i,
        human_unit(in_total),
        human_unit(out_total),
        human_unit(isec as i64),
        human_unit(osec as i64),
        human_unit(imin as i64),
        human_unit(omin as i64),
    );
}

fn print_stats(total: &str, h: &BTreeMap<String, DeviceHist>) {
    let cls = "\x1Bc";
    println!(
        "{}{:17} {:8} {:8} | {:8} | {:8} | {:8} | {:8}  ",
        cls, "interface", "in", "out", "in/sec", "out/sec", "in/min", "out/min"
    );

    let mut list: Vec<String> = vec![];
    for k in h.keys() {
        if k == total {
            continue;
        }
        list.push(k.to_string());
    }
    list.push(total.to_string());

    for k in &list {
        let dl = h.get(k).unwrap();
        let history_len = dl.history.len();

        let in_total = dl.history[history_len - 1].ibytes;
        let out_total = dl.history[history_len - 1].obytes;

        let isec = if history_len > 1 {
            dl.history[history_len - 1].ibytes - dl.history[history_len - 2].ibytes
        } else {
            0
        };

        let osec = if history_len > 1 {
            dl.history[history_len - 1].obytes - dl.history[history_len - 2].obytes
        } else {
            0
        };

        let imin = if history_len > 1 {
            dl.history[history_len - 1].ibytes - dl.history[0].ibytes as i64
        } else {
            0 as i64
        };

        let omin = if history_len > 1 {
            dl.history[history_len - 1].obytes - dl.history[0].obytes as i64
        } else {
            0 as i64
        };

        print_line_stats(&k, in_total, out_total, isec, osec, imin, omin);
    }
    println!();
}

fn print_graph(total: &str, h: &BTreeMap<String, DeviceHist>, height: i32) {
    if !h.contains_key(total) {
        return;
    }

    let mut max_delta: i64 = 0;
    let history_list = &h.get(total).unwrap().history;

    if history_list.len() < 2 {
        return;
    }

    // get the limit for the graph
    for hist in 1..history_list.len() {
        let cur_total = history_list[hist].ibytes + history_list[hist].obytes;
        let last_total = history_list[hist - 1].ibytes + history_list[hist - 1].obytes;
        if cur_total - last_total > max_delta {
            max_delta = cur_total - last_total;
        }
    }

    // iterate lines as we draw down
    for line in 1..height {
        print!("{:10}", "");
        for hist in 1..history_list.len() {
            let last_total = history_list[hist - 1].ibytes + history_list[hist - 1].obytes;
            let total = history_list[hist].ibytes + history_list[hist].obytes;
            let diff = total - last_total;
            print!(
                "{}",
                if diff >= (max_delta / height as i64) * (height as i64 - line as i64) {
                    "#"
                } else {
                    " "
                }
            );
        }
        println!();
    }
}

fn run_loops() {
    let total = "total";
    let mut ifs: BTreeMap<String, DeviceHist> = BTreeMap::new();

    loop {
        let now = std::time::Instant::now();
        let mut dh: BTreeMap<String, bool> = BTreeMap::new();
        let devs = read_proc_net();

        if !ifs.contains_key(total) {
            ifs.insert(
                total.to_string(),
                DeviceHist {
                    history: Vec::new(),
                },
            );
        }

        let mut ibytes: i64 = 0;
        let mut obytes: i64 = 0;
        let mut ipackets: i64 = 0;
        let mut opackets: i64 = 0;

        for dev in devs.iter() {
            if !ifs.contains_key(&dev.name) {
                ifs.insert(
                    dev.name.to_string(),
                    DeviceHist {
                        history: Vec::new(),
                    },
                );
            }

            let h = ifs.get_mut(&dev.name).unwrap();
            let hh = &mut h.history;

            hh.push(Device {
                name: dev.name.to_string(),
                ibytes: dev.ibytes,
                obytes: dev.obytes,
                ipackets: dev.ipackets,
                opackets: dev.opackets,
            });
            dh.insert(dev.name.to_string(), true);
            while hh.len() > 60 {
                hh.remove(0);
            }

            ibytes += dev.ibytes;
            obytes += dev.obytes;
            ipackets += dev.ipackets;
            opackets += dev.opackets;
        }

        let total_if = ifs.get_mut(total).unwrap();
        let total_history = &mut total_if.history;

        total_history.push(Device {
            name: total.to_string(),
            ibytes,
            obytes,
            ipackets,
            opackets,
        });
        while total_history.len() > 60 {
            total_history.remove(0);
        }
        dh.insert(total.to_string(), true);

        let mut remove_v: Vec<String> = Vec::new();
        for ifp in ifs.keys() {
            if !dh.contains_key(ifp) {
                remove_v.push(ifp.to_string());
            }
        }
        for v in remove_v {
            ifs.remove(&v);
        }

        print_stats(total, &ifs);
        print_graph(total, &ifs, 10);

        loop {
            thread::sleep(std::time::Duration::from_millis(100));
            if now.elapsed() >= std::time::Duration::from_millis(1000) {
                break;
            }
        }
    }
}

fn main() {
    run_loops();
}

#[test]
fn test_human_units() {
    assert_eq!(human_unit(1024), "1 KiB");
    assert_eq!(human_unit(1024 * 2), "2 KiB");
    assert_eq!(human_unit(1024 * 1024), "1 MiB");
    assert_eq!(human_unit(1024 * 1024 * 1024), "1 GiB");
    assert_eq!(human_unit(1024 * 1024 * 1024 * 1024), "1 TiB");
    assert_eq!(human_unit(1024 * 1024 * 1024 * 1024 * 1024), "1 PiB");
}
