//! Path normalization for kernel file I/O.
//!
//! Guests pass file paths inside an `OBJECT_ATTRIBUTES` struct that points at
//! an `ANSI_STRING` descriptor. Those paths come in several Xbox-flavored
//! forms — NT device paths (`\Device\Cdrom0\...`), drive letters (`D:\...`,
//! `d:\...`), or symbolic link prefixes (`game:\...`). We strip whichever
//! prefix applies and return a plain slash-separated path relative to the
//! mounted VFS root, so `VfsDevice::read_file` can look it up directly.

use xenia_memory::{GuestMemory, MemoryAccess};

/// Xbox `ANSI_STRING`:
///   u16 Length
///   u16 MaximumLength
///   u32 Buffer (guest pointer)
pub fn read_ansi_string(mem: &GuestMemory, ptr: u32) -> Option<String> {
    if ptr == 0 {
        return None;
    }
    let length = mem.read_u16(ptr) as u32;
    let buffer = mem.read_u32(ptr + 4);
    if buffer == 0 || length == 0 {
        return Some(String::new());
    }
    let mut out = String::with_capacity(length as usize);
    for i in 0..length {
        let c = mem.read_u8(buffer + i);
        if c == 0 {
            break;
        }
        out.push(c as char);
    }
    Some(out)
}

/// Xbox `OBJECT_ATTRIBUTES`:
///   u32 RootDirectory (handle)
///   u32 Name          (pointer to ANSI_STRING)
///   u32 Attributes
fn read_object_attributes_name(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option<String> {
    if obj_attrs_ptr == 0 {
        return None;
    }
    let name_ptr = mem.read_u32(obj_attrs_ptr + 4);
    read_ansi_string(mem, name_ptr)
}

/// Known Xbox device prefixes that need to be stripped before looking a path
/// up in the VFS. The list mirrors the symbolic links xenia-canary sets up
/// at boot (see `xboxkrnl_io.cc`). Case-insensitive matching.
const DEVICE_PREFIXES: &[&str] = &[
    "\\Device\\Cdrom0\\",
    "\\Device\\Harddisk0\\Partition1\\",
    "\\Device\\Harddisk0\\Partition0\\",
    "\\Device\\Harddisk0\\",
    "\\Device\\Mu0\\",
    "\\Device\\Mu1\\",
    "\\Device\\Mass0\\",
    "\\Device\\Mass1\\",
    "\\Device\\Mass2\\",
    "\\SystemRoot\\",
    "\\??\\",
    "game:\\",
    "d:\\",
    "D:\\",
];

/// Strip any Xbox device prefix and normalize backslashes to forward slashes.
/// Returns the path relative to the VFS root.
pub fn normalize_path(raw: &str) -> String {
    let mut s = raw.trim().to_string();

    // Case-insensitive prefix strip.
    let lowered = s.to_ascii_lowercase();
    for prefix in DEVICE_PREFIXES {
        let pl = prefix.to_ascii_lowercase();
        if lowered.starts_with(&pl) {
            s = s[pl.len()..].to_string();
            break;
        }
    }

    // Drop any leading slash/backslash that survived prefix stripping.
    while s.starts_with('\\') || s.starts_with('/') {
        s.remove(0);
    }

    // Canonical form: forward slashes.
    s.replace('\\', "/")
}

/// Convenience: read the OBJECT_ATTRIBUTES struct at `obj_attrs_ptr` and
/// return a normalized VFS path. Returns `None` if the struct pointer or its
/// inner name pointer is null.
pub fn object_attributes_to_vfs_path(mem: &GuestMemory, obj_attrs_ptr: u32) -> Option<String> {
    let raw = read_object_attributes_name(mem, obj_attrs_ptr)?;
    if raw.is_empty() {
        return None;
    }
    Some(normalize_path(&raw))
}

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

    #[test]
    fn strips_device_cdrom() {
        assert_eq!(normalize_path("\\Device\\Cdrom0\\default.xex"), "default.xex");
    }

    #[test]
    fn strips_drive_letter_lowercase() {
        assert_eq!(normalize_path("d:\\media\\shared\\foo.pkg"), "media/shared/foo.pkg");
    }

    #[test]
    fn strips_drive_letter_uppercase() {
        assert_eq!(normalize_path("D:\\default.xex"), "default.xex");
    }

    #[test]
    fn strips_game_prefix() {
        assert_eq!(normalize_path("game:\\data\\whatever.bin"), "data/whatever.bin");
    }

    #[test]
    fn preserves_relative_path() {
        assert_eq!(normalize_path("scripts/init.lua"), "scripts/init.lua");
    }

    #[test]
    fn handles_partition1() {
        assert_eq!(
            normalize_path("\\Device\\Harddisk0\\Partition1\\content\\abc.sav"),
            "content/abc.sav"
        );
    }
}
