use core::time::Duration;
use getopts::Options;
use linescroll::*;
use nix::fcntl::*;
use nix::poll::*;
use nix::sys::time::*;
use std::fs::File;
use std::io::prelude::*;
use std::io::SeekFrom;
use std::os::unix::io::AsRawFd;
use std::os::unix::io::RawFd;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::time::SystemTime;
use std::{str, thread, time};

fn banner() -> String {
    format!(
        "{} version {}",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION")
    )
}

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

fn general_options(args: Vec<String>, fw_list: &mut Vec<FileWatch>) -> Settings {
    let mut opts = Options::new();
    opts.parsing_style(getopts::ParsingStyle::FloatingFrees);
    opts.optflag("c", "combine", "combine all file source metrics");
    opts.optopt(
        "l",
        "limit",
        "stop after number of iterations (default never)",
        "ITERATIONS",
    );
    opts.optflag("s", "speedonly", "no graph, speed only");
    opts.optopt("p", "print", "print only every Nth iteration", "ITERATIONS");
    opts.optflag("f", "filename", "print filename headings");
    opts.optflag("h", "help", "print usage help");
    opts.optflag("n", "noclear", "do not clear screen between draws");
    opts.optflag("a", "noaxislimit", "do not print axis limit");
    opts.optflag("", "nofollow", "do not follow file renames");
    opts.optflag("v", "version", "display version number");
    opts.optflag("r", "raw", "input is metric");
    opts.optflag("e", "exit", "exit on failure");

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

    if matches.opt_present("help") {
        println!("{}", opts.usage(&banner()));
        std::process::exit(0);
    }

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

    let mut settings = Settings {
        combine: false,
        speedonly: false,
        print_every: 1,
        limit: 0,
        filenames: false,
        noclear: false,
        nofollow: false,
        raw: false,
        exit: false,
        axislimit: true,
    };

    if matches.opt_present("print") {
        settings.print_every = match matches.opt_str("print").unwrap().parse::<usize>() {
            Ok(n) => n,
            Err(x) => {
                eprintln!("can't convert to numeric: {}", x);
                std::process::exit(1);
            }
        };
    };

    settings.filenames = matches.opt_present("f");
    settings.combine = matches.opt_present("c");
    settings.noclear = matches.opt_present("n");
    settings.nofollow = matches.opt_present("nofollow");
    settings.raw = matches.opt_present("raw");
    settings.exit = matches.opt_present("exit");
    settings.axislimit = !matches.opt_present("noaxislimit");

    if matches.opt_present("limit") {
        settings.limit = match matches.opt_str("limit").unwrap().parse::<usize>() {
            Ok(n) => n,
            Err(x) => {
                eprintln!("can't convert to numeric: {}", x);
                std::process::exit(1);
            }
        };
    };

    if matches.opt_present("speedonly") {
        settings.speedonly = true;
    }

    let free_args = matches.free;
    if !free_args.is_empty() {
        for free in free_args {
            let f = FileWatch {
                count: Arc::new(Mutex::new(0_u64)),
                minute: vec![],
                five_minute: vec![],
                fifteen_minute: vec![],
                name: free.clone(),
            };

            if f.name == "-" {
                spawn_reader(Arc::clone(&f.count), None, settings.clone());
            } else {
                spawn_reader(Arc::clone(&f.count), Some(free), settings.clone());
            }
            fw_list.push(f);
        }
    } else {
        let f = FileWatch {
            count: Arc::new(Mutex::new(0_u64)),
            minute: vec![],
            five_minute: vec![],
            fifteen_minute: vec![],
            name: "stdin".to_string(),
        };

        spawn_reader(Arc::clone(&f.count), None, settings.clone());
        fw_list.push(f);
    }
    settings
}

fn print_stats(
    settings: &Settings,
    item: &FileWatch,
    counter: u64,
    iterations: usize,
    axislimit: bool,
) {
    if !should_print(settings, iterations) {
        return;
    }

    println!(
        "{}{:width$}/sec {:width$}/min {:width$}/5min {:width$}/15min",
        filename_print(settings, item),
        counter,
        average_set(&item.minute),
        average_set(&item.five_minute),
        average_set(&item.fifteen_minute),
        width = 6
    );

    if !settings.speedonly {
        print!("{}", graph(&item.minute, axislimit));
    }
}

fn main() {
    let mut fw_list: Vec<FileWatch> = vec![];
    let args: Vec<String> = std::env::args().collect();
    let settings = general_options(args, &mut fw_list);
    let mut iterations: usize = 0;

    let clear_and_top = "\x1Bc".to_string();
    let mut combine: u64 = 0;
    let mut combine_item = FileWatch {
        count: Arc::new(Mutex::new(0_u64)),
        minute: vec![],
        five_minute: vec![],
        fifteen_minute: vec![],
        name: "[combined input]".to_string(),
    };

    loop {
        sleep(1000);
        iterations += 1;
        if should_print(&settings, iterations) && should_clear(&settings) {
            println!("{}", clear_and_top);
        }

        let mut counter;

        if settings.combine {
            combine = 0;
        }

        for item in fw_list.iter_mut() {
            trim_item(item, 59);

            let mut counter_lock = item.count.lock().unwrap();
            counter = *counter_lock;
            *counter_lock = 0;

            item.minute.push(counter);
            item.five_minute.push(counter);
            item.fifteen_minute.push(counter);

            if !settings.combine {
                print_stats(&settings, item, counter, iterations, settings.axislimit)
            } else {
                combine += counter;
            }
        }
        if settings.combine {
            trim_item(&mut combine_item, 59);
            combine_item.minute.push(combine);
            combine_item.five_minute.push(combine);
            combine_item.fifteen_minute.push(combine);

            print_stats(
                &settings,
                &combine_item,
                combine,
                iterations,
                settings.axislimit,
            )
        }

        if settings.limit > 0 && iterations == settings.limit {
            break;
        }
    }
}

fn line_val(u: &str) -> u64 {
    match u.trim().parse::<u64>() {
        Ok(n) => n,
        Err(_x) => {
            // eprintln!("couldn't read {}: {}", u, x);
            // return 0;
            0
        }
    }
}

fn spawn_reader(count: Arc<Mutex<u64>>, path: Option<String>, settings: Settings) {
    thread::spawn(move || {
        let settings = settings;
        let mut inode_changed = false;
        loop {
            fn update_counters(
                fd: RawFd,
                count: &Arc<Mutex<u64>>,
                path: &Option<String>,
                nofollow: bool,
                raw: bool,
                fail_exit: bool,
            ) {
                let mut counter: u64 = 0;
                let original_inode = match nix::sys::stat::fstat(fd) {
                    Ok(i) => Some(i.st_ino),
                    Err(_) => None,
                };

                if fcntl(fd, FcntlArg::F_SETFL(OFlag::O_NONBLOCK)).is_err() {
                    println!("could not set fd to non-blocking");
                }

                let pfds_orig = vec![PollFd::new(fd, PollFlags::POLLIN)];

                loop {
                    let now = SystemTime::now();
                    let mut buffer = [0; 4096];

                    'reader: loop {
                        if now.elapsed().unwrap().as_millis() >= 100 {
                            break;
                        }
                        let mut pfds = pfds_orig.clone();
                        let timespec = Some(TimeSpec::from(Duration::new(0, 500000000)));
                        match ppoll(&mut pfds, timespec, None) {
                            Ok(_y) => {
                                if let Some(x) = pfds[0].revents() {
                                    if x | PollFlags::POLLIN == PollFlags::POLLIN {
                                        while let Ok(size) = nix::unistd::read(fd, &mut buffer[..])
                                        {
                                            if raw {
                                                let s: String = str::from_utf8(
                                                    &buffer
                                                        .iter()
                                                        .cloned()
                                                        .take(size)
                                                        .collect::<Vec<u8>>(),
                                                )
                                                .unwrap()
                                                .to_string();
                                                for l in s.split('\n') {
                                                    if l.is_empty() {
                                                        continue;
                                                    }
                                                    counter += line_val(l);
                                                }
                                            } else {
                                                for x in buffer.iter().take(size) {
                                                    if *x == b'\n' {
                                                        counter += 1;
                                                    }
                                                }
                                            }
                                            if size != 4096 {
                                                if size == 0 {
                                                    sleep(200);
                                                }
                                                break 'reader;
                                            }
                                        }
                                    }
                                }
                            }
                            Err(x) => {
                                println!("Error polling: {}", x);
                            }
                        }
                    }

                    let mut counter_lock = count.lock().unwrap();
                    *counter_lock += counter;
                    counter = 0;

                    if !nofollow && path.is_some() {
                        if let Some(oi) = original_inode {
                            let path = path.clone().unwrap();
                            let stat_source = Path::new(&path);
                            match nix::sys::stat::stat(stat_source) {
                                Ok(r) => {
                                    if r.st_ino != oi {
                                        return;
                                    }
                                }
                                Err(x) => {
                                    if fail_exit {
                                        eprintln!("Cannot stat {}:{}", path, x);
                                        std::process::exit(1);
                                    }
                                }
                            }
                        }
                    }
                }
            }

            match path {
                Some(ref p) => {
                    let f = File::open(p);

                    if f.is_err() {
                        if settings.exit {
                            eprintln!("Cannot open {}", p);
                            std::process::exit(1);
                        } else {
                            sleep(100);
                            continue;
                        }
                    }
                    let mut f = f.unwrap();
                    match f.seek(if inode_changed {
                        SeekFrom::Start(0)
                    } else {
                        SeekFrom::End(0)
                    }) {
                        Ok(_) => {}
                        Err(_x) => {
                            // might be /dev/fd
                            // eprintln!("Cannot seek {}: {}", p, x);
                            // std::process::exit(1);
                        }
                    }

                    let v = f.as_raw_fd();
                    update_counters(
                        v,
                        &count,
                        &path,
                        settings.nofollow,
                        settings.raw,
                        settings.exit,
                    );
                    inode_changed = true;
                }
                None => {
                    let v = std::io::stdin().as_raw_fd();
                    update_counters(
                        v,
                        &count,
                        &path,
                        settings.nofollow,
                        settings.raw,
                        settings.exit,
                    );
                }
            }
        }
    });
}

fn sleep(millis: u64) {
    let duration = time::Duration::from_millis(millis);
    thread::sleep(duration);
}
