use std::fs::File;

use anyhow::Result;

use simplelog::{debug, error, info};

use super::{generate_files, Entries, Entry};

use crate::builder::FileBuilderEnum;
use crate::config::Operation::*;
use crate::config::{self, Config};
use crate::os::Process;

pub fn dump_offsets(
    process: &Process,
    builders: &mut Vec<FileBuilderEnum>,
    file_path: &str,
    indent: usize,
) -> Result<()> {
    let file = File::open(config::OFFSETS_CONF)?;

    let config: Config = serde_json::from_reader(file)?;

    info!("Dumping offsets...");

    let mut entries = Entries::new();

    for signature in config.signatures {
        debug!("Searching for <bright-yellow>{}</>...", signature.name);

        let module = process
            .get_module_by_name(&signature.module)
            .expect(&format!("Failed to find module {}.", signature.module));

        let mut address = match process.find_pattern(&signature.module, &signature.pattern) {
            Some(a) => a,
            None => {
                error!(
                    "Failed to find pattern for <bright-yellow>{}</>.",
                    signature.name
                );

                continue;
            }
        };

        for operation in signature.operations {
            match operation {
                Add { value } => address += value,
                Deref { times, size } => {
                    let times = times.unwrap_or(1);
                    let size = size.unwrap_or(8);

                    for _ in 0..times {
                        process.read_memory_raw(address, &mut address as *mut _ as *mut _, size)?;
                    }
                }
                Jmp { offset, length } => {
                    address = process.resolve_jmp(address, offset, length)?.into();
                }
                Rip { offset, length } => {
                    address = process.resolve_rip(address, offset, length)?.into()
                }
                Slice { start, end } => {
                    let mut result: usize = 0;

                    process.read_memory_raw(
                        address + start,
                        &mut result as *mut _ as *mut _,
                        end - start,
                    )?;

                    address = result.into();
                }
                Sub { value } => address -= value,
            }
        }

        let (name, value) = if address < module.base() {
            debug!(
                "Found <bright-yellow>{}</> @ <bright-blue>{:#X}</>",
                signature.name, address
            );

            (signature.name, address)
        } else {
            debug!(
                "Found <bright-yellow>{}</> @ <bright-magenta>{:#X}</> (<blue>{}</> + <bright-blue>{:#X}</>)",
                signature.name,
                address,
                signature.module,
                address - module.base()
            );

            (signature.name, address - module.base())
        };

        if name == "dwBuildNumber" {
            let build_number = process.read_memory::<u32>(module.base() + value)?;

            debug!("Game build number: <bright-yellow>{}</>", build_number);

            let container = entries.entry("game_info".to_string()).or_default();

            container.comment =
                Some("Some additional information about the game at dump time".to_string());

            container.data.push(Entry {
                name: "buildNumber".to_string(),
                value: build_number as usize,
                comment: Some("Game build number".to_string()),
                indent: Some(indent),
            });
        }

        let container = entries
            .entry(signature.module.replace(".", "_"))
            .or_default();

        container.comment = Some(signature.module);

        container.data.push(Entry {
            name,
            value,
            comment: None,
            indent: Some(indent),
        });
    }

    generate_files(builders, &entries, file_path, "offsets")?;

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;

    use std::ffi::{c_char, c_void};
    use std::fs;
    use std::mem::offset_of;
    use std::thread;
    use std::time::Duration;

    use core::arch::x86_64::_bittest;

    use anyhow::anyhow;

    use serde_json::Value;

    fn read_json_value(file_path: &str) -> Result<Value> {
        let content = fs::read_to_string(file_path)?;

        serde_json::from_str(&content).map_err(Into::into)
    }

    fn get_class_field(module_name: &str, class_name: &str, class_key: &str) -> Result<u64> {
        let value = read_json_value(&format!("generated/{}.json", module_name))
            .expect("unable to read json file");

        value[class_name]["data"][class_key]["value"]
            .as_u64()
            .ok_or_else(|| {
                anyhow!(
                    "unable to find class field {} in class {}",
                    class_key,
                    class_name
                )
            })
    }

    fn get_offset_value(module_name: &str, offset_name: &str) -> Result<u64> {
        let value = read_json_value("generated/offsets.json").expect("unable to read offsets.json");

        value[module_name.replace(".", "_")]["data"][offset_name]["value"]
            .as_u64()
            .ok_or_else(|| anyhow!("unable to find offset"))
    }

    #[test]
    fn build_number() -> Result<()> {
        let process = Process::new("cs2.exe")?;

        let engine_base = process
            .get_module_by_name("engine2.dll")
            .expect("unable to find engine2.dll")
            .base();

        let build_number_offset = get_offset_value("engine2.dll", "dwBuildNumber")?;

        let build_number =
            process.read_memory::<u32>(engine_base + build_number_offset as usize)?;

        println!("build number: {}", build_number);

        Ok(())
    }

    #[test]
    fn key_buttons() -> Result<()> {
        let process = Process::new("cs2.exe")?;

        let client_base = process
            .get_module_by_name("client.dll")
            .expect("unable to find client.dll")
            .base();

        const KEY_BUTTONS: [&str; 8] = [
            "dwForceAttack",
            "dwForceAttack2",
            "dwForceBackward",
            "dwForceCrouch",
            "dwForceForward",
            "dwForceJump",
            "dwForceLeft",
            "dwForceRight",
        ];

        let get_key_state = |value: u32| match value {
            256 => "key up",
            65537 => "key down",
            _ => "unknown",
        };

        // Sleep for a second, so we're able to test.
        thread::sleep(Duration::from_secs(1));

        for button in &KEY_BUTTONS {
            let offset = get_offset_value("client.dll", button).expect("unable to find client.dll");

            let value = process.read_memory::<u32>(client_base + offset as usize)?;

            println!("key button: {} (state: {})", button, get_key_state(value));
        }

        Ok(())
    }

    #[test]
    fn global_vars() -> Result<()> {
        #[derive(Debug)]
        #[repr(C)]
        struct GlobalVarsBase {
            real_time: f32,                  // 0x0000
            frame_count: i32,                // 0x0004
            frame_time: f32,                 // 0x0008
            absolute_frame_time: f32,        // 0x000C
            max_clients: i32,                // 0x0010
            pad_0: [u8; 0x14],               // 0x0014
            frame_time_2: f32,               // 0x0028
            current_time: f32,               // 0x002C
            current_time_2: f32,             // 0x0030
            pad_1: [u8; 0xC],                // 0x0034
            tick_count: f32,                 // 0x0040
            pad_2: [u8; 0x4],                // 0x0044
            network_channel: *const c_void,  // 0x0048
            pad_3: [u8; 0x130],              // 0x0050
            current_map: *const c_char,      // 0x0180
            current_map_name: *const c_char, // 0x0188
        }

        impl GlobalVarsBase {
            fn current_map(&self, process: &Process) -> Result<String> {
                let name_ptr = process.read_memory::<usize>(
                    (self as *const _ as usize + offset_of!(Self, current_map)).into(),
                )?;

                process.read_string(name_ptr.into())
            }

            fn current_map_name(&self, process: &Process) -> Result<String> {
                let name_ptr = process.read_memory::<usize>(
                    (self as *const _ as usize + offset_of!(Self, current_map_name)).into(),
                )?;

                process.read_string(name_ptr.into())
            }
        }

        let process = Process::new("cs2.exe")?;

        let client_base = process
            .get_module_by_name("client.dll")
            .expect("unable to find client.dll")
            .base();

        let global_vars_offset = get_offset_value("client.dll", "dwGlobalVars")?;

        let global_vars = process
            .read_memory::<*const GlobalVarsBase>(client_base + global_vars_offset as usize)?;

        let current_map_name = unsafe {
            (*global_vars)
                .current_map_name(&process)
                .unwrap_or_default()
        };

        println!("current map name: {}", current_map_name);

        Ok(())
    }

    #[test]
    fn is_key_down() -> Result<()> {
        let process = Process::new("cs2.exe")?;

        let input_system_base = process
            .get_module_by_name("inputsystem.dll")
            .expect("unable to find inputsystem.dll")
            .base();

        let input_system_offset = get_offset_value("inputsystem.dll", "dwInputSystem")?;

        let input_system = input_system_base + input_system_offset as usize;

        let is_key_down = |key_code: i32| -> bool {
            let element = process
                .read_memory::<i32>((input_system + 0x4 * (key_code as usize / 32) + 0x12A0).into())
                .unwrap_or_default();

            unsafe { _bittest(&element, key_code & 0x1F) != 0 }
        };

        // Sleep for a second, so we're able to test.
        thread::sleep(Duration::from_secs(1));

        // See https://www.unknowncheats.me/forum/3855779-post889.html for button codes.
        println!("insert key down: {}", is_key_down(73));

        Ok(())
    }

    #[test]
    fn local_player_controller() -> Result<()> {
        let process = Process::new("cs2.exe")?;

        let client_base = process
            .get_module_by_name("client.dll")
            .expect("unable to find client.dll")
            .base();

        let local_player_controller_offset =
            get_offset_value("client.dll", "dwLocalPlayerController")?;

        let player_name_offset =
            get_class_field("client.dll", "CBasePlayerController", "m_iszPlayerName")?;

        let local_player_controller =
            process.read_memory::<usize>(client_base + local_player_controller_offset as usize)?;

        let player_name =
            process.read_string((local_player_controller + player_name_offset as usize).into())?;

        println!("local player name: {}", player_name);

        Ok(())
    }

    #[test]
    fn local_player_pawn() -> Result<()> {
        #[derive(Debug)]
        #[repr(C)]
        struct Vector3D {
            x: f32,
            y: f32,
            z: f32,
        }

        let process = Process::new("cs2.exe")?;

        let client_base = process
            .get_module_by_name("client.dll")
            .expect("unable to find client.dll")
            .base();

        let local_player_pawn_offset = get_offset_value("client.dll", "dwLocalPlayerPawn")?;

        let game_scene_node_offset =
            get_class_field("client.dll", "C_BaseEntity", "m_pGameSceneNode")?;

        let absolute_origin_offset =
            get_class_field("client.dll", "CGameSceneNode", "m_vecAbsOrigin")?;

        let local_player_pawn =
            process.read_memory::<usize>(client_base + local_player_pawn_offset as usize)?;

        let game_scene_node = process
            .read_memory::<usize>((local_player_pawn + game_scene_node_offset as usize).into())?;

        let absolute_origin = process
            .read_memory::<Vector3D>((game_scene_node + absolute_origin_offset as usize).into())?;

        println!("local player origin: {:?}", absolute_origin);

        Ok(())
    }

    #[test]
    fn window_size() -> Result<()> {
        let process = Process::new("cs2.exe")?;

        let engine_base = process
            .get_module_by_name("engine2.dll")
            .expect("unable to find engine2.dll")
            .base();

        let window_width_offset = get_offset_value("engine2.dll", "dwWindowWidth")?;
        let window_height_offset = get_offset_value("engine2.dll", "dwWindowHeight")?;

        let window_width =
            process.read_memory::<u32>(engine_base + window_width_offset as usize)?;

        let window_height =
            process.read_memory::<u32>(engine_base + window_height_offset as usize)?;

        println!("window size: {}x{}", window_width, window_height);

        Ok(())
    }
}