use std::io;
use std::io::{BufRead, BufReader, Error, ErrorKind, Read};
use std::fs::File;
use std::process::exit;
use std::time::Instant;
use argparse::{ArgumentParser, StoreTrue, Store};

mod day1;
mod day2;
mod day3;
mod day4;
mod day5;
mod day6;
mod day7;
mod day8;
mod day9;
mod day10;

static NUM_DAYS: i32 = 10;

fn day_switch<T: BufRead>(day: i32, reader: T) -> io::Result<(String, String)> {
    match day {
        1 => day1::main(reader),
        2 => day2::main(reader),
        3 => day3::main(reader),
        4 => day4::main(reader),
        5 => day5::main(reader),
        6 => day6::main(reader),
        7 => day7::main(reader),
        8 => day8::main(reader),
        9 => day9::main(reader),
        10 => day10::main(reader),
        _ => Err(Error::new(ErrorKind::Other, "Invalid day"))
    }
}

fn file_for_day(day: i32) -> io::Result<BufReader<File>> {
    match File::open(format!("input/{}.txt", day)) {
        Ok(f) => Ok(BufReader::new(f)),
        Err(_) => Err(Error::new(ErrorKind::Other, format!("No input file for day {}", day)))
    }
}

fn benchmark_day(day: i32) -> io::Result<f64> {
    let num_iters = 100;

    let mut input = Vec::new();
    file_for_day(day)?.read_to_end(&mut input)?;

    let start = Instant::now();
    for _i in 0..num_iters {
        day_switch(day, BufReader::new(&input[..]))?;
    }
    let end = Instant::now();

    let dur = (end - start) / num_iters;
    let secs = dur.as_secs() as f64 + dur.subsec_micros() as f64 / 1000000.0;

    println!("Day {}: {} secs", day, secs);

    Ok(secs)
}

fn benchmark_all_days() -> io::Result<()> {
    let mut total = 0.0;
    for day in 1..NUM_DAYS + 1 {
        total += benchmark_day(day)?;
    }
    println!("Total: {} secs", total);
    Ok(())
}

struct Options {
    use_stdin: bool,
    bench: bool,
}

fn run_day(day: i32, opts: &Options) -> io::Result<()> {
    let (part1, part2) = if opts.bench {
        benchmark_day(day)?;
        return Ok(())
    } else if opts.use_stdin {
        day_switch(day, BufReader::new(io::stdin()))?
    } else {
        day_switch(day, file_for_day(day)?)?
    };

    println!("{}", part1);
    println!("{}", part2);
    Ok(())
}

fn run_all_days(opts: &Options) -> io::Result<()> {
    if opts.bench {
        assert!(!opts.use_stdin);
        benchmark_all_days()
    } else {
        for day in 1..NUM_DAYS + 1 {
            run_day(day, &opts)?;
        }
        Ok(())
    }
}

fn error_handler<F>(func: F) -> io::Result<()>
        where F: Fn() -> io::Result<()> {

    match func() {
        Ok(()) => Ok(()),
        Err(err) => {
            if err.kind() == ErrorKind::Other {
                println!("Error: {}", err);
                exit(1)
            } else {
                Err(err)
            }
        }
    }
}

fn main() -> io::Result<()> {
    let mut day_string = String::new();
    let mut options = Options {
        use_stdin: false,
        bench: false,
    };

    {
        let mut parser = ArgumentParser::new();
        parser.set_description("AOC 2018 solutions by Tom Smeding");
        parser.refer(&mut options.use_stdin)
            .add_option(&["-s", "--stdin"], StoreTrue, "Read from stdin");
        parser.refer(&mut options.bench)
            .add_option(&["-b", "--bench"], StoreTrue, "Benchmark the given days");
        parser.refer(&mut day_string)
            .add_argument("day", Store, "Day to execute");
        parser.parse_args_or_exit();
    }

    error_handler(||
        if day_string.len() == 0 {
            run_all_days(&options)
        } else {
            match day_string.parse::<i32>() {
                Ok(day) => run_day(day, &options),
                Err(_) => {
                    println!("Invalid day argument");
                    exit(1)
                }
            }
        }
    )
}