use sdl2;
use sdl2::controller::Button;
use sdl2::event::{Event, WindowEvent};
use sdl2::image::{InitFlag, LoadSurface, LoadTexture};
use sdl2::keyboard::Scancode;
use sdl2::messagebox::*;
use sdl2::pixels::Color;
use sdl2::rect::Rect;
use sdl2::render::WindowCanvas;
use sdl2::surface::Surface;

use sdl2::EventPump;

use bytesize;
use spin_sleep;

use std::cell::RefCell;
use std::rc::Rc;

use std::fs;
use std::io::{Cursor, Read};
use std::path::{Path, PathBuf};
use std::process;
use std::time;

use std::convert::TryFrom;

#[macro_use]
extern crate clap;

#[macro_use]
extern crate log;
use flexi_logger;
use flexi_logger::*;

use reqwest;

mod audio;
mod input;
mod video;

use audio::create_audio_player;
use input::create_input;
use video::{create_video_interface, SCREEN_HEIGHT, SCREEN_WIDTH};

use rustboyadvance_core::cartridge::BackupType;
use rustboyadvance_core::prelude::*;
use rustboyadvance_core::util::spawn_and_run_gdb_server;
use rustboyadvance_core::util::FpsCounter;

const LOG_DIR: &str = ".logs";
const DEFAULT_GDB_SERVER_ADDR: &'static str = "localhost:1337";

const CANVAS_WIDTH: u32 = SCREEN_WIDTH;
const CANVAS_HEIGHT: u32 = SCREEN_HEIGHT;

fn get_savestate_path(rom_filename: &Path) -> PathBuf {
    rom_filename.with_extension("savestate")
}

/// Waits for the user to drag a rom file to window
fn wait_for_rom(canvas: &mut WindowCanvas, event_pump: &mut EventPump) -> Result<String, String> {
    let texture_creator = canvas.texture_creator();
    let icon_texture = texture_creator
        .load_texture("assets/icon_cropped_small.png")
        .expect("failed to load icon");
    let background = Color::RGB(0xDD, 0xDD, 0xDD);

    let mut redraw = || -> Result<(), String> {
        canvas.set_draw_color(background);
        canvas.clear();
        canvas.copy(
            &icon_texture,
            None,
            Some(Rect::from_center(
                ((CANVAS_WIDTH / 2) as i32, (CANVAS_HEIGHT / 2) as i32),
                160,
                100,
            )),
        )?;
        canvas.present();
        Ok(())
    };

    redraw()?;

    loop {
        for event in event_pump.wait_iter() {
            match event {
                Event::DropFile { filename, .. } => {
                    return Ok(filename);
                }
                Event::Quit { .. } => process::exit(0),
                Event::Window { win_event, .. } => match win_event {
                    WindowEvent::SizeChanged(..) | WindowEvent::Restored => redraw()?,
                    _ => {}
                },
                _ => {}
            }
        }
    }
}

fn ask_download_bios() -> Result<Option<Vec<u8>>, Box<dyn std::error::Error>> {
    const OPEN_SOURCE_BIOS_URL: &'static str =
        "https://github.com/Nebuleon/ReGBA/raw/master/bios/gba_bios.bin";

    let buttons: Vec<_> = vec![
        ButtonData {
            flags: MessageBoxButtonFlag::NOTHING,
            button_id: 100,
            text: "No, Exit!",
        },
        ButtonData {
            flags: MessageBoxButtonFlag::NOTHING,
            button_id: 101,
            text: "Yes",
        },
    ];

    let res = show_message_box(
        MessageBoxFlag::WARNING,
        buttons.as_slice(),
        "bios not found",
        "would you like to me download an open source bios for you?",
        None,
        None,
    )?;

    match res {
        ClickedButton::CloseButton => Ok(None),
        ClickedButton::CustomButton(button) => match button.button_id {
            101 => {
                let mut resp = reqwest::blocking::get(OPEN_SOURCE_BIOS_URL)?;
                let mut bios = Vec::with_capacity(16 * 1024);
                resp.read_to_end(&mut bios)?;
                show_simple_message_box(
                    MessageBoxFlag::INFORMATION,
                    "Download Finished!",
                    "Download Finished!",
                    None,
                )?;
                Ok(Some(bios))
            }
            _ => Ok(None),
        },
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    fs::create_dir_all(LOG_DIR).expect(&format!("could not create log directory ({})", LOG_DIR));
    flexi_logger::Logger::with_env_or_str("info")
        .log_to_file()
        .directory(LOG_DIR)
        .duplicate_to_stderr(Duplicate::Debug)
        .format_for_files(default_format)
        .format_for_stderr(colored_default_format)
        .start()
        .unwrap();

    let mut frame_limiter = true;
    let yaml = load_yaml!("cli.yml");
    let matches = clap::App::from_yaml(yaml).get_matches();

    let skip_bios = matches.occurrences_of("skip_bios") != 0;

    let debug = matches.occurrences_of("debug") != 0;
    let with_gdbserver = matches.occurrences_of("with_gdbserver") != 0;

    info!("Initializing SDL2 context");
    let sdl_context = sdl2::init().expect("failed to initialize sdl2");

    let bios_path = Path::new(matches.value_of("bios").unwrap_or_default());
    let bios_bin = match read_bin_file(bios_path) {
        Ok(bios) => bios,
        _ => match ask_download_bios() {
            Ok(Some(bios)) => {
                info!("saving downloaded bios to {}", bios_path.display());
                write_bin_file(bios_path, &bios)?;

                bios
            }
            Ok(None) => {
                info!("Exiting");
                std::process::exit(0);
            }
            Err(e) => {
                error!("error when downloading bios: {}", e);
                std::process::exit(1);
            }
        },
    };

    let mut event_pump = sdl_context.event_pump()?;

    let video_subsystem = sdl_context.video()?;
    let _image_context = sdl2::image::init(InitFlag::PNG | InitFlag::JPG)?;
    let mut window = video_subsystem
        .window("RustBoyAdvance", SCREEN_WIDTH * 3, SCREEN_HEIGHT * 3)
        .opengl()
        .position_centered()
        .resizable()
        .build()?;

    let window_icon = Surface::from_file("assets/icon.png")?;
    window.set_icon(window_icon);

    let mut canvas = window.into_canvas().accelerated().build()?;
    canvas.set_logical_size(CANVAS_WIDTH, CANVAS_HEIGHT)?;

    let controller_subsystem = sdl_context.game_controller()?;
    let controller_mappings =
        include_str!("../../../external/SDL_GameControllerDB/gamecontrollerdb.txt");
    controller_subsystem.load_mappings_from_read(&mut Cursor::new(controller_mappings))?;

    let available_controllers = (0..controller_subsystem.num_joysticks()?)
        .filter(|&id| controller_subsystem.is_game_controller(id))
        .collect::<Vec<u32>>();

    let _active_controller = match available_controllers.first() {
        Some(&id) => {
            let controller = controller_subsystem.open(id)?;
            info!("Found game controller: {}", controller.name());
            Some(controller)
        }
        _ => {
            info!("No game controllers were found");
            None
        }
    };

    let mut rom_path = match matches.value_of("game_rom") {
        Some(path) => path.to_string(),
        _ => {
            info!("[!] Rom file missing, please drag a rom file into the emulator window...");
            wait_for_rom(&mut canvas, &mut event_pump)?
        }
    };

    let video = Rc::new(RefCell::new(create_video_interface(canvas)));
    let audio = Rc::new(RefCell::new(create_audio_player(&sdl_context)));
    let input = Rc::new(RefCell::new(create_input()));

    let mut savestate_path = get_savestate_path(&Path::new(&rom_path));

    let mut rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();

    let gamepak = GamepakBuilder::new()
        .save_type(BackupType::try_from(
            matches.value_of("save_type").unwrap(),
        )?)
        .file(Path::new(&rom_path))
        .build()?;

    let mut gba = GameBoyAdvance::new(
        bios_bin.into_boxed_slice(),
        gamepak,
        video.clone(),
        audio.clone(),
        input.clone(),
    );

    if skip_bios {
        gba.skip_bios();
    }

    if debug {
        #[cfg(feature = "debugger")]
        {
            gba.cpu.set_verbose(true);
            let mut debugger = Debugger::new(gba);
            info!("starting debugger...");
            debugger.repl(matches.value_of("script_file")).unwrap();
            info!("ending debugger...");
            return Ok(());
        }
        #[cfg(not(feature = "debugger"))]
        {
            panic!("Please compile me with 'debugger' feature");
        }
    }

    if with_gdbserver {
        spawn_and_run_gdb_server(&mut gba, DEFAULT_GDB_SERVER_ADDR)?;
    }

    let mut fps_counter = FpsCounter::default();
    let frame_time = time::Duration::new(0, 1_000_000_000u32 / 60);
    'running: loop {
        let start_time = time::Instant::now();

        for event in event_pump.poll_iter() {
            match event {
                Event::KeyDown {
                    scancode: Some(scancode),
                    ..
                } => match scancode {
                    Scancode::Space => frame_limiter = false,
                    k => input.borrow_mut().on_keyboard_key_down(k),
                },
                Event::KeyUp {
                    scancode: Some(scancode),
                    ..
                } => match scancode {
                    #[cfg(feature = "debugger")]
                    Scancode::F1 => {
                        let mut debugger = Debugger::new(gba);
                        info!("starting debugger...");
                        debugger.repl(matches.value_of("script_file")).unwrap();
                        gba = debugger.gba;
                        info!("ending debugger...")
                    }
                    #[cfg(feature = "gdb")]
                    Scancode::F2 => spawn_and_run_gdb_server(&mut gba, DEFAULT_GDB_SERVER_ADDR)?,
                    Scancode::F5 => {
                        info!("Saving state ...");
                        let save = gba.save_state()?;
                        write_bin_file(&savestate_path, &save)?;
                        info!(
                            "Saved to {:?} ({})",
                            savestate_path,
                            bytesize::ByteSize::b(save.len() as u64)
                        );
                    }
                    Scancode::F9 => {
                        if savestate_path.is_file() {
                            let save = read_bin_file(&savestate_path)?;
                            info!("Restoring state from {:?}...", savestate_path);
                            gba.restore_state(&save)?;
                            info!("Restored!");
                        } else {
                            info!("Savestate not created, please create one by pressing F5");
                        }
                    }
                    Scancode::Space => frame_limiter = true,
                    k => input.borrow_mut().on_keyboard_key_up(k),
                },
                Event::ControllerButtonDown { button, .. } => match button {
                    Button::RightStick => frame_limiter = !frame_limiter,
                    b => input.borrow_mut().on_controller_button_down(b),
                },
                Event::ControllerButtonUp { button, .. } => {
                    input.borrow_mut().on_controller_button_up(button);
                }
                Event::ControllerAxisMotion { axis, value, .. } => {
                    input.borrow_mut().on_axis_motion(axis, value);
                }
                Event::Quit { .. } => break 'running,
                Event::DropFile { filename, .. } => {
                    // load the new rom
                    rom_path = filename;
                    savestate_path = get_savestate_path(&Path::new(&rom_path));
                    rom_name = Path::new(&rom_path).file_name().unwrap().to_str().unwrap();
                    let gamepak = GamepakBuilder::new().file(Path::new(&rom_path)).build()?;
                    let bios_bin = read_bin_file(bios_path).unwrap();

                    // create a new emulator - TODO, export to a function
                    gba = GameBoyAdvance::new(
                        bios_bin.into_boxed_slice(),
                        gamepak,
                        video.clone(),
                        audio.clone(),
                        input.clone(),
                    );
                    gba.skip_bios();
                }
                _ => {}
            }
        }

        gba.frame();

        if let Some(fps) = fps_counter.tick() {
            let title = format!("{} ({} fps)", rom_name, fps);
            video.borrow_mut().set_window_title(&title);
        }

        if frame_limiter {
            let time_passed = start_time.elapsed();
            let delay = frame_time.checked_sub(time_passed);
            match delay {
                None => {}
                Some(delay) => {
                    spin_sleep::sleep(delay);
                }
            };
        }
    }

    Ok(())
}
