use crate::device;
use crate::netplay;
use crate::ui;
use slint::Model;

slint::include_modules!();

pub const N64_EXTENSIONS: [&str; 12] = [
    "n64", "v64", "z64", "7z", "zip", "bin", "N64", "V64", "Z64", "7Z", "ZIP", "BIN",
];

#[derive(serde::Deserialize)]
struct GithubData {
    tag_name: String,
}

pub struct NetplayDevice {
    pub peer_addr: std::net::SocketAddr,
    pub player_number: u8,
}

pub struct GbPaths {
    pub rom: [Option<std::path::PathBuf>; 4],
    pub ram: [Option<std::path::PathBuf>; 4],
}

#[derive(Clone)]
pub struct GameSettings {
    pub fullscreen: bool,
    pub overclock: bool,
    pub disable_expansion_pak: bool,
    pub cheats: std::collections::HashMap<String, Option<String>>,
}

fn check_latest_version(weak: slint::Weak<AppWindow>) {
    let client = reqwest::Client::builder()
        .user_agent(env!("CARGO_PKG_NAME"))
        .build()
        .unwrap();
    let task = client
        .get("https://api.github.com/repos/gopher64/gopher64/releases/latest")
        .send();
    tokio::spawn(async move {
        let response = task.await;
        if let Ok(response) = response {
            let data: Result<GithubData, reqwest::Error> = response.json().await;

            let latest_version = if let Ok(data) = data {
                semver::Version::parse(&data.tag_name[1..]).unwrap()
            } else {
                semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap()
            };
            let current_version = semver::Version::parse(env!("CARGO_PKG_VERSION")).unwrap();
            if current_version < latest_version {
                weak.upgrade_in_event_loop(move |handle| handle.set_has_update(true))
                    .unwrap();
            }
        }
    });
}

fn local_game_window(app: &AppWindow, controller_paths: &[Option<String>], usb: ui::Usb) {
    let dirs = ui::get_dirs();
    let weak = app.as_weak();
    let controller_paths = controller_paths.to_owned();
    app.on_open_rom_button_clicked(move || {
        let controller_paths = controller_paths.clone();
        let mut usb_tx = None;
        if let Some(usb_tx_inner) = usb.usb_tx.as_ref() {
            usb_tx = Some(usb_tx_inner.clone());
        }
        let mut cart_rx = None;
        if let Some(cart_rx_inner) = usb.cart_rx.as_ref() {
            cart_rx = Some(cart_rx_inner.resubscribe());
        }
        weak.upgrade_in_event_loop(move |handle| {
            save_settings(&handle, &controller_paths);
            open_rom(&handle, ui::Usb { usb_tx, cart_rx })
        })
        .unwrap();
    });

    let saves_path = dirs.data_dir.join("saves");
    app.on_saves_folder_button_clicked(move || {
        open::that_detached(saves_path.clone()).unwrap();
    });
}

fn input_profiles(config: &ui::config::Config) -> Vec<String> {
    let mut profiles = vec![];
    for key in config.input.input_profiles.keys() {
        profiles.push(key.clone())
    }
    profiles
}

fn settings_window(app: &AppWindow, config: &ui::config::Config) {
    app.set_integer_scaling(config.video.integer_scaling);
    app.set_fullscreen(config.video.fullscreen);
    app.set_widescreen(config.video.widescreen);
    app.set_apply_crt_shader(config.video.crt);
    app.set_overclock_n64_cpu(config.emulation.overclock);
    app.set_disable_expansion_pak(config.emulation.disable_expansion_pak);
    app.set_emulate_usb(config.emulation.usb);
    let combobox_value = match config.video.upscale {
        1 => 0,
        2 => 1,
        4 => 2,
        8 => 3,
        _ => 0,
    };
    app.set_resolution(combobox_value);

    if let Some(rom_dir_str) = config.rom_dir.to_str() {
        app.set_rom_dir(rom_dir_str.into());
    }
}

fn update_input_profiles(weak: &slint::Weak<AppWindow>, config: &ui::config::Config) {
    let profiles = input_profiles(config);
    let config_bindings = config.input.input_profile_binding.clone();
    let weak2 = weak.clone();
    weak.upgrade_in_event_loop(move |handle| {
        let profile_bindings = slint::VecModel::default();
        for (i, input_profile_binding) in handle.get_selected_profile_binding().iter().enumerate() {
            let currently_selected = handle
                .get_input_profiles()
                .row_data(input_profile_binding as usize)
                .unwrap_or(config_bindings[i].clone().into())
                .to_string();
            let position = profiles
                .iter()
                .position(|profile| *profile == currently_selected);
            profile_bindings.push(position.unwrap() as i32);
        }

        let input_profiles = slint::VecModel::default();
        for profile in profiles {
            input_profiles.push(profile.into());
        }
        let input_profiles_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
            std::rc::Rc::new(input_profiles);
        handle.set_input_profiles(slint::ModelRc::from(input_profiles_model));

        let input_profile_binding_model: std::rc::Rc<slint::VecModel<i32>> =
            std::rc::Rc::new(profile_bindings);
        handle.set_selected_profile_binding(slint::ModelRc::from(input_profile_binding_model));

        // this is a workaround to make the input profile combobox update
        handle.set_blank_profiles(true);
        slint::Timer::single_shot(std::time::Duration::from_millis(200), move || {
            weak2
                .upgrade_in_event_loop(move |handle| {
                    handle.set_blank_profiles(false);
                })
                .unwrap();
        });
    })
    .unwrap();
}

fn controller_window(
    app: &AppWindow,
    config: &ui::config::Config,
    controller_names: &Vec<String>,
    controller_paths: &[Option<String>],
) {
    let controller_enabled_model: std::rc::Rc<slint::VecModel<bool>> = std::rc::Rc::new(
        slint::VecModel::from(config.input.controller_enabled.to_vec()),
    );
    app.set_emulate_vru(config.input.emulate_vru);

    app.set_controller_enabled(slint::ModelRc::from(controller_enabled_model));

    let transferpak_enabled_model: std::rc::Rc<slint::VecModel<bool>> =
        std::rc::Rc::new(slint::VecModel::from(config.input.transfer_pak.to_vec()));
    app.set_transferpak(slint::ModelRc::from(transferpak_enabled_model));

    update_input_profiles(&app.as_weak(), config);

    let controllers = slint::VecModel::default();
    for controller in controller_names {
        controllers.push(controller.into());
    }
    let controller_names_model: std::rc::Rc<slint::VecModel<slint::SharedString>> =
        std::rc::Rc::new(controllers);
    app.set_controller_names(slint::ModelRc::from(controller_names_model));

    let controller_changed = slint::VecModel::default();
    let selected_controllers = slint::VecModel::default();
    for selected in config.input.controller_assignment.iter() {
        let mut found = false;
        for (i, path) in controller_paths.iter().enumerate() {
            if selected == path {
                selected_controllers.push(i as i32);
                found = true;
                continue;
            }
        }
        if !found {
            selected_controllers.push(0);
        }
        controller_changed.push(false);
    }
    let selected_controllers_model: std::rc::Rc<slint::VecModel<i32>> =
        std::rc::Rc::new(selected_controllers);
    app.set_selected_controller(slint::ModelRc::from(selected_controllers_model));

    let controller_changed_model: std::rc::Rc<slint::VecModel<bool>> =
        std::rc::Rc::new(controller_changed);
    app.set_controller_changed(slint::ModelRc::from(controller_changed_model));

    let weak_app = app.as_weak();
    app.on_input_profile_button_clicked(move || {
        let dialog = InputProfileDialog::new().unwrap();
        dialog.set_deadzone(ui::input::DEADZONE_DEFAULT);
        let weak_dialog = dialog.as_weak();
        let weak_app = weak_app.clone();
        dialog.on_profile_creation_button_clicked(move || {
            let weak_app = weak_app.clone();
            weak_dialog
                .upgrade_in_event_loop(move |handle| {
                    handle.hide().unwrap();
                    let profile_name = handle.get_profile_name().into();
                    let dinput = handle.get_dinput();
                    let deadzone = handle.get_deadzone();

                    tokio::spawn(async move {
                        let mut game_ui = ui::Ui::new();
                        ui::input::configure_input_profile(
                            &mut game_ui,
                            profile_name,
                            dinput,
                            deadzone,
                        );
                        update_input_profiles(&weak_app, &game_ui.config);
                    });
                })
                .unwrap();
        });
        dialog.show().unwrap();
    });
}

pub fn save_settings(app: &AppWindow, controller_paths: &[Option<String>]) {
    let mut config = ui::config::Config::new();
    config.video.integer_scaling = app.get_integer_scaling();
    config.video.fullscreen = app.get_fullscreen();
    config.video.widescreen = app.get_widescreen();
    config.video.crt = app.get_apply_crt_shader();
    config.emulation.overclock = app.get_overclock_n64_cpu();
    config.emulation.disable_expansion_pak = app.get_disable_expansion_pak();
    config.emulation.usb = app.get_emulate_usb();
    let upscale_values = [1, 2, 4, 8];
    config.video.upscale = upscale_values[app.get_resolution() as usize];

    config.input.emulate_vru = app.get_emulate_vru();
    for (i, controller_enabled) in app.get_controller_enabled().iter().enumerate() {
        config.input.controller_enabled[i] = controller_enabled;
    }
    for (i, transferpak_enabled) in app.get_transferpak().iter().enumerate() {
        config.input.transfer_pak[i] = transferpak_enabled;
    }
    for (i, input_profile_binding) in app.get_selected_profile_binding().iter().enumerate() {
        config.input.input_profile_binding[i] = app
            .get_input_profiles()
            .row_data(input_profile_binding as usize)
            .unwrap()
            .to_string();
    }

    for (i, selected_controller) in app.get_selected_controller().iter().enumerate() {
        if app.get_controller_changed().row_data(i).unwrap_or(false) {
            config.input.controller_assignment[i] =
                controller_paths[selected_controller as usize].clone();
        }
    }
}

fn about_window(app: &AppWindow) {
    app.on_wiki_button_clicked(move || {
        open::that_detached("https://github.com/gopher64/gopher64/wiki").unwrap();
    });
    app.on_discord_button_clicked(move || {
        open::that_detached("https://discord.gg/9RGXq8W8JQ").unwrap();
    });
    app.on_patreon_button_clicked(move || {
        open::that_detached("https://patreon.com/loganmc10").unwrap();
    });
    app.on_github_sponsors_button_clicked(move || {
        open::that_detached("https://github.com/sponsors/loganmc10").unwrap();
    });
    app.on_source_code_button_clicked(move || {
        open::that_detached("https://github.com/gopher64/gopher64").unwrap();
    });
    app.on_newversion_button_clicked(move || {
        open::that_detached("https://github.com/gopher64/gopher64/releases/latest").unwrap();
    });
    app.set_version(format!("Version: {}", env!("CARGO_PKG_VERSION")).into());
    check_latest_version(app.as_weak());
}

pub fn app_window() {
    let app = AppWindow::new().unwrap();
    about_window(&app);
    let mut usb = ui::Usb {
        usb_tx: None,
        cart_rx: None,
    };
    let mut shutdown_tx = None;
    let mut controller_paths;
    {
        let game_ui = ui::Ui::new();
        let mut controller_names = ui::input::get_controller_names(&game_ui);
        controller_names.insert(0, "None".into());
        controller_paths = ui::input::get_controller_paths(&game_ui);
        controller_paths.insert(0, None);
        settings_window(&app, &game_ui.config);
        controller_window(&app, &game_ui.config, &controller_names, &controller_paths);

        if game_ui.config.emulation.usb {
            (shutdown_tx, usb) = ui::usb::init(app.as_weak());
        }
    }
    local_game_window(&app, &controller_paths, usb);
    ui::netplay::netplay_window(&app, &controller_paths);
    ui::cheats::cheats_window(&app);
    app.run().unwrap();
    if let Some(shutdown_tx) = &shutdown_tx {
        ui::usb::close(shutdown_tx);
    }
    save_settings(&app, &controller_paths);
}

pub fn run_rom(
    gb_paths: GbPaths,
    file_path: std::path::PathBuf,
    mut game_settings: GameSettings,
    netplay: Option<NetplayDevice>,
    usb: ui::Usb,
    weak: slint::Weak<AppWindow>,
) {
    tokio::spawn(async move {
        weak.upgrade_in_event_loop(move |handle| handle.set_game_running(true))
            .unwrap();

        let mut device = device::Device::new();
        device.ui.config.rom_dir = file_path.parent().unwrap().to_path_buf();

        for i in 0..4 {
            if gb_paths.rom[i].is_some() && gb_paths.ram[i].is_some() {
                device.transferpaks[i].cart.rom =
                    std::fs::read(gb_paths.rom[i].as_ref().unwrap()).unwrap();

                device.transferpaks[i].cart.ram =
                    std::fs::read(gb_paths.ram[i].as_ref().unwrap()).unwrap();
            }
        }

        device.ui.usb.usb_tx = usb.usb_tx;
        device.ui.usb.cart_rx = usb.cart_rx;

        device.ui.weak = Some(weak.clone());

        if let Some(rom_contents) = device::get_rom_contents(file_path.as_path()) {
            if let Some(netplay_device) = netplay {
                device.netplay = Some(netplay::init(
                    netplay_device.peer_addr,
                    netplay_device.player_number,
                ));
            } else {
                game_settings.cheats = {
                    let game_crc = ui::storage::get_game_crc(&rom_contents);
                    let cheats = ui::config::Cheats::new();
                    cheats.cheats.get(&game_crc).cloned().unwrap_or_default()
                };
            }
            device::run_game(&mut device, rom_contents, game_settings);
            if device.netplay.is_some() {
                netplay::close(&mut device);
            }
        } else {
            println!("Could not read rom file");
        }

        let rom_dir = device.ui.config.rom_dir.clone();
        weak.upgrade_in_event_loop(move |handle| {
            if let Some(rom_dir_str) = rom_dir.to_str() {
                handle.set_rom_dir(rom_dir_str.into());
            }
            handle.set_game_running(false);
        })
        .unwrap();
    });
}

fn open_rom(app: &AppWindow, usb: ui::Usb) {
    let rom_dir = app.get_rom_dir();
    let select_rom = if !rom_dir.is_empty()
        && let Ok(exists) = std::fs::exists(&rom_dir)
        && exists
    {
        rfd::AsyncFileDialog::new().set_directory(rom_dir)
    } else {
        rfd::AsyncFileDialog::new()
    }
    .set_title("Select ROM")
    .add_filter("ROM files", &N64_EXTENSIONS)
    .pick_file();
    let mut select_gb_rom = [None, None, None, None];
    let mut select_gb_ram = [None, None, None, None];

    for (i, transfer_pak_enabled) in app.get_transferpak().iter().enumerate() {
        if transfer_pak_enabled {
            select_gb_rom[i] = Some(
                rfd::AsyncFileDialog::new()
                    .set_title(format!("GB ROM P{}", i + 1))
                    .add_filter("GB ROM files", &["gb", "gbc", "GB", "GBC"])
                    .pick_file(),
            );
            select_gb_ram[i] = Some(
                rfd::AsyncFileDialog::new()
                    .set_title(format!("GB RAM P{}", i + 1))
                    .add_filter("GB RAM files", &["sav", "ram", "srm", "SAV", "RAM", "SRM"])
                    .pick_file(),
            );
        }
    }

    let fullscreen = app.get_fullscreen();
    let overclock = app.get_overclock_n64_cpu();
    let disable_expansion_pak = app.get_disable_expansion_pak();

    let weak = app.as_weak();
    tokio::spawn(async move {
        if let Some(file) = select_rom.await {
            let mut gb_rom_path = [None, None, None, None];
            let mut gb_ram_path = [None, None, None, None];

            for i in 0..4 {
                if let (Some(gb_rom), Some(gb_ram)) =
                    (select_gb_rom[i].as_mut(), select_gb_ram[i].as_mut())
                    && let (Some(gb_rom), Some(gb_ram)) = (gb_rom.await, gb_ram.await)
                {
                    gb_rom_path[i] = Some(gb_rom.path().to_path_buf());
                    gb_ram_path[i] = Some(gb_ram.path().to_path_buf());
                }
            }

            run_rom(
                GbPaths {
                    rom: gb_rom_path,
                    ram: gb_ram_path,
                },
                file.path().to_path_buf(),
                GameSettings {
                    fullscreen,
                    overclock,
                    disable_expansion_pak,
                    cheats: std::collections::HashMap::new(), // will be filled in later
                },
                None,
                usb,
                weak,
            );
        }
    });
}
