use getopts::Options;
#[cfg(not(target_os = "linux"))]
use libc::*;
use std::collections::BTreeMap;
#[cfg(not(target_os = "linux"))]
use std::ffi;
use std::thread;

use chrono::{DateTime, Local, Timelike};

use netr::*;

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),
        human_unit(osec),
        human_unit(imin),
        human_unit(omin),
    );
}

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
        } else {
            0_i64
        };

        let omin = if history_len > 1 {
            dl.history[history_len - 1].obytes - dl.history[0].obytes
        } else {
            0_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,
    indent: usize,
    print_max: bool,
) {
    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!("{:indent$}", "", indent = indent);
        for hist in 1..history_list.len() {
            let last_total = history_list[hist - 1].ibytes + history_list[hist - 1].obytes;
            let last_in_diff = history_list[hist].ibytes - history_list[hist - 1].ibytes;
            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) {
                    if last_in_diff >= (max_delta / height as i64) * (height as i64 - line as i64) {
                        "I"
                    } else {
                        "O"
                    }
                } else {
                    " "
                }
            );
        }

        if line == 1 && print_max {
            print!(
                "{:padding$} {:>9}",
                "",
                human_unit(max_delta),
                padding = 61 - history_list.len()
            );
        }

        println!();
    }
}

fn print_minute_marker(
    total: &str,
    h: &BTreeMap<String, DeviceHist>,
    time: chrono::DateTime<Local>,
    indent: usize,
) {
    let padding = h.get(total).unwrap().history.len() as u32;
    if padding > time.second() {
        let padding = padding - time.second() - 2;
        println!(
            "{:indent$}{:padding$}^ {:02}:{:02}",
            "",
            "",
            time.hour(),
            time.minute(),
            padding = padding as usize
        );
    }
}

fn run_loops(config: &Config) {
    let total = "total";
    let indent = 10;
    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_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 config.filter.is_some() {
                let c = config.clone().filter.unwrap();
                if !c.matches(&dev.name) {
                    continue;
                }
            }

            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() > 61 {
                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() > 61 {
            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, indent, true);

        let time: DateTime<Local> = Local::now();
        print_minute_marker(total, &ifs, time, indent);

        loop {
            thread::sleep(std::time::Duration::from_millis(100));
            if now.elapsed() >= std::time::Duration::from_millis(1000) {
                break;
            }
        }
    }
}
fn banner() -> String {
    format!(
        "{} version {}",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION")
    )
}

fn print_version() {
    println!("{}", &banner());
}

fn help(opts: &getopts::Options) {
    println!("{}", opts.usage(&banner()));
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let mut config = Config::new();
    let mut opts = Options::new();
    opts.parsing_style(getopts::ParsingStyle::FloatingFrees);
    opts.optopt("e", "exclude", "exclude this pattern", "REGEX");
    opts.optopt("i", "include", "include this pattern", "REGEX");
    opts.optflag("h", "help", "display help");
    opts.optflag("v", "version", "display version");

    let matches = match opts.parse(&args[1..]) {
        Ok(m) => m,
        Err(f) => {
            println!("{}", f);
            std::process::exit(1);
        }
    };

    if matches.opt_present("version") {
        print_version();
        std::process::exit(0);
    }

    if matches.opt_present("help") {
        help(&opts);
        std::process::exit(0);
    }

    if matches.opt_present("exclude") {
        let pattern = matches.opt_str("exclude").unwrap();
        let filter = match FilterOpts::create(&pattern, !matches.opt_present("exclude")) {
            Some(x) => x,
            None => {
                eprintln!("Cannot create regex from {}", pattern);
                help(&opts);
                std::process::exit(1);
            }
        };

        config.filter = Some(filter);
    }

    if matches.opt_present("include") {
        if matches.opt_present("exclude") {
            eprintln!("Cannot --exclude and --include at the same time");
            help(&opts);
            std::process::exit(1);
        }

        let pattern = matches.opt_str("include").unwrap();
        let filter = match FilterOpts::create(&pattern, matches.opt_present("include")) {
            Some(x) => x,
            None => {
                eprintln!("Cannot create regex from {}", matches.free[0].clone());
                help(&opts);
                std::process::exit(1);
            }
        };

        config.filter = Some(filter);
    }

    run_loops(&config);
}
