use crate::device;
use crate::ui;

#[derive(serde::Serialize, serde::Deserialize, Copy, Clone)]
pub struct DecodedCheat {
    code_type: u8,
    address: u32,
    data: u16,
}

#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Cheats {
    pub cheats: Vec<Vec<DecodedCheat>>,
    pub boot: bool,
    pub enabled: bool,
}

#[derive(serde::Serialize, serde::Deserialize, Clone)]
pub struct CheatData {
    pub note: String,
    pub data: Vec<String>,
    pub options: Option<std::collections::BTreeMap<String, String>>,
}

pub type Cheat = std::collections::BTreeMap<String, std::collections::BTreeMap<String, CheatData>>;

pub fn init(
    device: &mut device::Device,
    cheat_settings: rustc_hash::FxHashMap<String, Option<String>>,
) {
    let cheats = serde_json::from_slice::<Cheat>(include_bytes!("../data/cheats.json"))
        .unwrap()
        .get(&ui::storage::get_game_crc(&device.cart.rom))
        .cloned()
        .unwrap_or_default();

    let re = regex::Regex::new(r"\?+").unwrap();
    for cheat_setting in cheat_settings.iter() {
        if let Some(cheat_data) = cheats.get(cheat_setting.0) {
            let mut option_value = None;
            if let Some(option) = cheat_setting.1 {
                if let Some(found_option_value) = cheat_data
                    .options
                    .as_ref()
                    .cloned()
                    .unwrap_or_default()
                    .get(option)
                {
                    option_value = Some(found_option_value.to_string());
                } else {
                    eprintln!("Cheat option: {option} not found");
                    continue;
                }
            }

            println!(
                "Cheat: {} Option: {}",
                cheat_setting.0,
                cheat_setting.1.clone().unwrap_or("none".to_string())
            );
            let mut decoded_cheat: Vec<DecodedCheat> = vec![];
            for code in cheat_data.data.iter() {
                let mut result = code.clone();
                if let Some(option_value) = option_value.as_ref() {
                    result = re.replace_all(code, option_value).into_owned();
                }
                let mut split = result.split_whitespace();
                let first_part = u32::from_str_radix(split.next().unwrap(), 16).unwrap();
                if let Ok(data) = u16::from_str_radix(split.next().unwrap(), 16) {
                    decoded_cheat.push(DecodedCheat {
                        code_type: (first_part >> 24) as u8,
                        address: first_part & 0x00FFFFFF,
                        data,
                    })
                } else {
                    eprintln!("Could not parse data for: {}", cheat_setting.0);
                };
            }
            if !decoded_cheat.is_empty() {
                device.cheats.cheats.push(decoded_cheat);
            }
        } else {
            eprintln!("Could not find cheat: {}", cheat_setting.0);
        }
    }

    if !device.cheats.cheats.is_empty() {
        device.cheats.enabled = true;
        ui::video::onscreen_message("Cheats enabled", ui::video::MESSAGE_LENGTH_MESSAGE_SHORT);
    }
}

fn write_byte(device: &mut device::Device, cheat_line: &DecodedCheat) {
    *device
        .rdram
        .mem
        .get_mut(cheat_line.address as usize ^ device.byte_swap)
        .unwrap_or(&mut 0) = cheat_line.data as u8;
}

fn write_half(device: &mut device::Device, cheat_line: &DecodedCheat) {
    *device
        .rdram
        .mem
        .get_mut(cheat_line.address as usize ^ device.byte_swap)
        .unwrap_or(&mut 0) = (cheat_line.data >> 8) as u8;
    *device
        .rdram
        .mem
        .get_mut((cheat_line.address + 1) as usize ^ device.byte_swap)
        .unwrap_or(&mut 0) = cheat_line.data as u8;
}

fn equal_byte(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
    let current_value = *device
        .rdram
        .mem
        .get(cheat_line.address as usize ^ device.byte_swap)
        .unwrap_or(&0);
    current_value == (cheat_line.data as u8)
}

fn equal_half(device: &mut device::Device, cheat_line: &DecodedCheat) -> bool {
    let current_value1 = *device
        .rdram
        .mem
        .get(cheat_line.address as usize ^ device.byte_swap)
        .unwrap_or(&0);
    let current_value2 = *device
        .rdram
        .mem
        .get((cheat_line.address + 1) as usize ^ device.byte_swap)
        .unwrap_or(&0);
    current_value1 == (cheat_line.data >> 8) as u8 && current_value2 == cheat_line.data as u8
}

pub fn execute_cheats(device: &mut device::Device, cheats: Vec<Vec<DecodedCheat>>) {
    for cheat in cheats.iter() {
        let mut valid = true;
        let mut cheat_iter = cheat.iter().peekable();
        while let Some(cheat_line) = cheat_iter.next() {
            match cheat_line.code_type {
                0x50 => {
                    if valid {
                        let mut expanded_cheat: Vec<DecodedCheat> = vec![];
                        let compressed_cheat = *cheat_iter.peek().unwrap();
                        let count = (cheat_line.address & 0xFF00) >> 8;
                        let offset = cheat_line.address & 0xFF;
                        for i in 0..count {
                            let line = DecodedCheat {
                                code_type: compressed_cheat.code_type,
                                address: compressed_cheat.address + (i * offset),
                                data: compressed_cheat.data
                                    + (i as i16 * cheat_line.data as i16) as u16,
                            };
                            expanded_cheat.push(line);
                        }
                        execute_cheats(device, [expanded_cheat].to_vec());
                    }
                    valid = false; // skip the next line
                }
                0x80 | 0xA0 => {
                    if valid {
                        write_byte(device, cheat_line);
                    }
                    valid = true;
                }
                0x81 | 0xA1 => {
                    if valid {
                        write_half(device, cheat_line);
                    }
                    valid = true;
                }
                0xF0 => {
                    if device.cheats.boot && valid {
                        write_byte(device, cheat_line);
                        valid = true;
                    }
                }
                0xF1 => {
                    if device.cheats.boot && valid {
                        write_half(device, cheat_line);
                        valid = true;
                    }
                }
                0xD0 => {
                    valid = equal_byte(device, cheat_line);
                }
                0xD1 => {
                    valid = equal_half(device, cheat_line);
                }
                0xD2 => {
                    valid = !equal_byte(device, cheat_line);
                }
                0xD3 => {
                    valid = !equal_half(device, cheat_line);
                }
                0x88 | 0x89 | 0xD8 | 0xD9 | 0xDA | 0xDB => { /* GS button not emulated */ }
                _ => panic!("Unknown cheat code type: {:X}", cheat_line.code_type),
            }
        }
    }
    device.cheats.boot = false;
}
