mod test;
mod ui;

use test::{results::Results, Test};

use crossterm::{
    self, cursor,
    event::{self, Event, KeyCode, KeyEvent, KeyModifiers},
    execute, terminal,
};
use rand::seq::SliceRandom;
use rand::thread_rng;
use std::fs;
use std::io::{self, BufRead};
use std::num;
use std::path::PathBuf;
use structopt::StructOpt;
use tui::{backend::CrosstermBackend, terminal::Terminal};

#[derive(Debug, StructOpt)]
#[structopt(name = "ttyper", about = "Terminal-based typing test.")]
struct Opt {
    #[structopt(parse(from_os_str))]
    contents: Option<PathBuf>,

    #[structopt(short, long)]
    debug: bool,

    #[structopt(short, long, default_value = "50")]
    words: num::NonZeroUsize,

    #[structopt(long, parse(from_os_str))]
    language_file: Option<PathBuf>,

    #[structopt(short, long, default_value = "english200")]
    language: String,
}

impl Opt {
    fn gen_contents(&self) -> Vec<String> {
        match &self.contents {
            Some(path) => {
                let file = fs::File::open(path).expect("Error reading language file.");
                let lines: Vec<String> = io::BufReader::new(file)
                    .lines()
                    .filter_map(Result::ok)
                    .collect();
                lines.iter().map(String::from).collect()
            }
            None => {
                let language: Vec<String> = {
                    let path = self.language_file.clone().unwrap_or_else(|| {
                        dirs::config_dir()
                            .expect("Couldn't find configuration directory.")
                            .join("ttyper")
                            .join("language")
                            .join(&self.language)
                    });
                    let file = fs::File::open(path).expect("Error reading language file.");
                    io::BufReader::new(file)
                        .lines()
                        .filter_map(Result::ok)
                        .collect()
                };

                let mut words: Vec<String> = language
                    .into_iter()
                    .cycle()
                    .take(self.words.into())
                    .collect();

                let mut rng = thread_rng();
                words.shuffle(&mut rng);

                words
            }
        }
    }
}

fn run_test(mut test: Test) -> crossterm::Result<bool> {
    let mut stdout = io::stdout();
    execute!(
        stdout,
        cursor::Hide,
        cursor::SavePosition,
        terminal::EnterAlternateScreen,
    )?;

    let backend = CrosstermBackend::new(stdout);
    let mut terminal = Terminal::new(backend)?;

    terminal.clear()?;
    terminal.draw(|f| {
        f.render_widget(&test, f.size());
    })?;

    // Enable raw mode to read keys
    terminal::enable_raw_mode()?;

    loop {
        match event::read()? {
            Event::Key(key) => {
                if key.modifiers.contains(KeyModifiers::CONTROL) {
                    if let KeyCode::Char('c') = key.code {
                        return Ok(false);
                    };
                }

                match key.code {
                    KeyCode::Esc => break,
                    _ => test.handle_key(key),
                }

                if test.complete {
                    break;
                }

                terminal.draw(|f| {
                    f.render_widget(&test, f.size());
                })?;
            }
            Event::Resize(_, _) => {
                terminal.draw(|f| {
                    f.render_widget(&test, f.size());
                })?;
            }
            _ => {}
        }
    }

    // Draw results
    let results = Results::from(test);
    terminal.clear()?;
    terminal.draw(|f| {
        f.render_widget(&results, f.size());
    })?;
    Ok(true)
}

fn exit() -> crossterm::Result<()> {
    terminal::disable_raw_mode()?;
    execute!(
        io::stdout(),
        cursor::RestorePosition,
        cursor::Show,
        terminal::LeaveAlternateScreen,
    )?;
    Ok(())
}

fn main() -> crossterm::Result<()> {
    let opt = Opt::from_args();

    loop {
        let contents = opt.gen_contents();
        if contents.is_empty() {
            println!("Test contents were empty. Exiting...");
            return Ok(());
        };

        if run_test(Test::new(contents))? {
            match event::read()? {
                Event::Key(KeyEvent {
                    code: KeyCode::Char('r'),
                    modifiers: KeyModifiers::NONE,
                }) => (),
                _ => break,
            }
        } else {
            return exit();
        }
    }
    exit()
}
