use c8::{Chip8, Display, KeyCode, HEIGHT, WIDTH};
use minifb::{Key, Scale, Window, WindowOptions};
use std::{
    sync::{mpsc::Sender, Arc},
    thread,
    time::Duration,
};

fn main() {
    // let program = include_bytes!("roms/IBM_Logo.ch8");
    // let program = include_bytes!("roms/bc_test.ch8");
    // let program = include_bytes!("roms/test_opcode.ch8");
    // let program = include_bytes!("roms/octojam2title.ch8");
    // let program = include_bytes!("roms/octojam3title.ch8");
    // let program = include_bytes!("roms/1dcell.ch8");
    // let program = include_bytes!("roms/snek.ch8");
    // let program = include_bytes!("roms/br8kout.ch8");
    // let program = include_bytes!("roms/4-flags.ch8");
    // let program = include_bytes!("roms/3-corax+.ch8");
    // let program = include_bytes!("roms/5-quirks.ch8");
    let program = include_bytes!("roms/6-keypad.ch8");
    let mut c8 = Chip8::new(program);
    run::<700>(&mut c8);
}

pub enum Event {
    Key(KeyCode),
    Timer,
    Tick,
}

pub fn run<const F: u64>(c8: &mut Chip8) {
    let (tx, rx) = std::sync::mpsc::channel();
    let tx1 = tx.clone();
    thread::spawn(move || {
        let millis = 1000000 / 60;
        let int = Duration::from_micros(millis);
        loop {
            thread::sleep(int);
            tx1.send(Event::Timer).unwrap();
        }
    });

    let tx2 = tx.clone();
    thread::spawn(move || {
        let millis = 1000000 / F;
        let int = Duration::from_micros(millis);
        loop {
            thread::sleep(int);
            tx2.send(Event::Tick).unwrap();
        }
    });

    show_display(c8.display.clone(), Some(tx));

    loop {
        c8.step();
        let event = rx.recv().unwrap();
        match event {
            Event::Key(k) => c8.set_key_pressed(k),
            Event::Timer => c8.decr_timers(),
            Event::Tick => {}
        }
    }
}

pub fn show_display(display: Arc<Display>, sender: Option<Sender<Event>>) {
    struct Input {
        sender: Sender<Event>,
    }

    impl Input {
        fn new(sender: Sender<Event>) -> Input {
            Input { sender }
        }
    }

    impl minifb::InputCallback for Input {
        fn add_char(&mut self, uni_char: u32) {
            if let Some(c) = char::from_u32(uni_char) {
                if let Ok(key) = c.try_into() {
                    self.sender.send(Event::Key(key)).unwrap();
                }
            }
        }
    }

    std::thread::spawn(move || {
        let mut window = Window::new(
            "Chip8",
            WIDTH,
            HEIGHT,
            WindowOptions {
                scale: Scale::X16,
                ..WindowOptions::default()
            },
        )
        .unwrap_or_else(|e| {
            panic!("{}", e);
        });

        // Limit to max ~60 fps update rate
        window.limit_update_rate(Some(std::time::Duration::from_micros(16600)));
        if let Some(sender) = sender {
            let input = Box::new(Input::new(sender));
            // Limit to max ~60 fps update rate
            window.set_input_callback(input);
        }

        while window.is_open() && !window.is_key_down(Key::Escape) {
            // We unwrap here as we want this code to exit if it fails. Real applications may want to handle this in a different way
            window
                .update_with_buffer(display.get_buffer(), WIDTH, HEIGHT)
                .unwrap();
        }
    });
}