//! Shared game loading and initialization for all Systemless frontends.
//!
//! Consolidates ROM loading (MacBinary + StuffIt), runner initialization,
//! and post-load configuration so all frontends behave identically.

use crate::loader::LoadedApp;
use crate::managers::resource::ResourceFork;
use crate::memory::MemoryBus;
use crate::runner::{FixtureRunner, FixtureRunnerConfig};
use stuffit::SitArchive;

const WEB_PACK_MAGIC: &[u8; 4] = b"KPK1";

/// Standard RAM size for all frontends (32 MB).
pub const RAM_SIZE: u32 = crate::machine_profile::ORACLE_MACHINE_PROFILE.ram_size_bytes;

/// Max instructions to execute per GUI/WASM frame.
/// Must be large enough to complete a full PICT draw (~500K instructions)
/// in one frame, otherwise the user sees partially-rendered intermediate states.
pub const MAX_INSTRUCTIONS_PER_FRAME: usize = 2_000_000;

/// Create a new FixtureRunner with standard configuration.
pub fn new_runner() -> FixtureRunner {
    FixtureRunner::new(
        RAM_SIZE as usize,
        FixtureRunnerConfig {
            load_address: 0x10000,
            max_instructions: MAX_INSTRUCTIONS_PER_FRAME,
            ..FixtureRunnerConfig::default()
        },
    )
}

/// Load a game ROM from raw file bytes (MacBinary, StuffIt archive, or raw resource fork).
///
/// Handles StuffIt archives (populates VFS with all entries, finds executable),
/// MacBinary files, and macOS resource fork paths. Returns the LoadedApp on success.
pub fn load_game(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
    if file_data.starts_with(WEB_PACK_MAGIC) {
        load_web_pack(runner, file_data)
    } else if is_stuffit_archive(file_data) {
        load_stuffit(runner, file_data)
    } else if crate::disk_image::looks_like_dc42_or_hfs(file_data) {
        load_disk_image(runner, file_data)
    } else {
        load_macbinary(runner, file_data)
    }
}

/// Prepack a StuffIt archive into a lightweight format for faster web startup.
///
/// The packed format stores fully decompressed data/resource forks for each file,
/// so loading avoids runtime archive decompression in Wasm.
pub fn pack_stuffit_for_web(file_data: &[u8]) -> Result<Vec<u8>, String> {
    let archive =
        SitArchive::parse(file_data).map_err(|e| format!("Failed to parse StuffIt: {:?}", e))?;

    let file_entries = collect_stuffit_payload_files(&archive)?;
    let mut out = Vec::new();
    out.extend_from_slice(WEB_PACK_MAGIC);
    out.extend_from_slice(&(file_entries.len() as u32).to_be_bytes());

    for entry in file_entries {
        let name_bytes = entry.name.as_bytes();
        if name_bytes.len() > u16::MAX as usize {
            return Err(format!(
                "Entry name too long for web pack: {} ({} bytes)",
                entry.name,
                name_bytes.len()
            ));
        }

        out.extend_from_slice(&(name_bytes.len() as u16).to_be_bytes());
        out.extend_from_slice(name_bytes);
        out.extend_from_slice(&entry.file_type);
        out.extend_from_slice(&(entry.data.len() as u32).to_be_bytes());
        out.extend_from_slice(&entry.data);
        out.extend_from_slice(&(entry.rsrc.len() as u32).to_be_bytes());
        out.extend_from_slice(&entry.rsrc);
    }

    Ok(out)
}

/// Load a game from a file path, trying macOS resource fork first, then MacBinary/StuffIt.
pub fn load_game_from_path(
    runner: &mut FixtureRunner,
    path: &std::path::Path,
) -> Result<LoadedApp, String> {
    let file_data =
        std::fs::read(path).map_err(|e| format!("Failed to read {}: {}", path.display(), e))?;

    // Explicit containers in the data fork win over any host macOS resource
    // fork. DiskCopy images extracted by unar, for example, can carry a small
    // Finder metadata resource fork on the host; that is not the launchable app.
    if file_data.starts_with(WEB_PACK_MAGIC)
        || is_stuffit_archive(&file_data)
        || crate::disk_image::looks_like_dc42_or_hfs(&file_data)
    {
        return load_game(runner, &file_data);
    }

    // Try loading resource fork from macOS extended attribute path first
    let rsrc_path = path.join("..namedfork/rsrc");
    if let Ok(rsrc_data) = std::fs::read(&rsrc_path) {
        if !rsrc_data.is_empty() {
            if crate::runner::trace_load_enabled() {
                eprintln!("[LOAD] Loading resource fork from {}", rsrc_path.display());
            }
            let app_name = path
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("FixtureGen");
            runner.dispatcher_mut().set_launched_app_path(app_name);
            let fork = ResourceFork::parse(&rsrc_data).ok_or("Failed to parse Resource Fork")?;
            return runner
                .load_app(&fork)
                .ok_or_else(|| "Failed to load app".to_string());
        }
    }

    // Fall back to detecting MacBinary/raw resource-fork style payloads.
    load_game(runner, &file_data)
}

/// Initialize a runner after loading: run init_app then clear the
/// screen so the initial framebuffer is a known state for screenshots.
pub fn init_game(runner: &mut FixtureRunner, app: &LoadedApp) {
    runner.init_app(app);

    {
        if runner.menu_bar_visible() {
            let (scrn_base, row_bytes, screen_width, screen_height, pixel_size) =
                runner.dispatcher().screen_mode;
            crate::trap::TrapDispatcher::fb_fill_pattern_rect(
                runner.bus_mut(),
                scrn_base,
                row_bytes,
                pixel_size,
                screen_width as i16,
                screen_height as i16,
                0,
                0,
                screen_height as i16,
                screen_width as i16,
                [0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55],
            );
            return;
        }

        // Clear screen memory to black.
        // For 8bpp, index 255 = black in the standard Mac CLUT.
        // For 1bpp, 0xFF = black (all bits set).
        let (scrn_base, row_bytes, _, scrn_height, _) = runner.dispatcher().screen_mode;
        let bus = runner.bus_mut();
        for i in 0..(row_bytes * scrn_height as u32) {
            bus.write_byte(scrn_base + i, 0xFF);
        }
    }
}

fn load_stuffit(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
    let archive =
        SitArchive::parse(file_data).map_err(|e| format!("Failed to parse StuffIt: {:?}", e))?;

    let mut executable_entry: Option<ExecutableCandidate> = None;

    for entry in &archive.entries {
        if entry.is_folder {
            // Register folder in VFS so directory lookups (e.g. Plug-Ins) succeed.
            let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&entry.name);
            runner.dispatcher_mut().ensure_vfs_directory(&normalized);
            continue;
        }

        let (data, rsrc) = entry
            .decompressed_forks()
            .map_err(|e| format!("Decompress error: {:?}", e))?;

        let payload = payload_from_forks(
            &entry.name,
            data,
            rsrc,
            entry.file_type,
            entry.creator,
            entry.finder_flags,
        )?;
        insert_payload_into_vfs(runner, payload, &mut executable_entry);
    }

    log_vfs(runner);

    let executable = executable_entry.ok_or("No executable found in archive")?;
    if crate::runner::trace_load_enabled() {
        eprintln!("[LOAD] Selected executable: {}", executable.name);
    }
    runner
        .dispatcher_mut()
        .set_launched_app_path(&executable.name);

    let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
    runner
        .load_app(&fork)
        .ok_or_else(|| "Failed to load app".to_string())
}

fn load_macbinary(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
    if file_data.len() < 128 {
        return Err("File too small for MacBinary".to_string());
    }

    let data_len =
        u32::from_be_bytes([file_data[83], file_data[84], file_data[85], file_data[86]]) as usize;
    let rsrc_len =
        u32::from_be_bytes([file_data[87], file_data[88], file_data[89], file_data[90]]) as usize;

    let data_start = 128;
    let data_end_padded = data_start + ((data_len + 127) & !127);
    let rsrc_start = data_end_padded;

    if rsrc_start + rsrc_len > file_data.len() {
        return Err("MacBinary truncated".to_string());
    }

    let data_end = data_start
        .checked_add(data_len)
        .ok_or_else(|| "MacBinary data offset overflow".to_string())?;
    let data_fork = &file_data[data_start..data_end];
    if is_stuffit_archive(data_fork) {
        if crate::runner::trace_load_enabled() {
            eprintln!("[LOAD] MacBinary data fork contains StuffIt archive");
        }
        return load_stuffit(runner, data_fork);
    }
    if crate::disk_image::looks_like_dc42_or_hfs(data_fork) {
        if crate::runner::trace_load_enabled() {
            eprintln!("[LOAD] MacBinary data fork contains HFS disk image");
        }
        return load_disk_image(runner, data_fork);
    }

    if crate::runner::trace_load_enabled() {
        eprintln!("[LOAD] Loading from MacBinary format");
    }

    // Extract the filename from MacBinary header (length at offset 1,
    // bytes at offset 2..65, max 63 chars) so GetAppParms / CurApName
    // have the app name to report.
    let name_len = (file_data[1] as usize).min(63);
    let name_bytes = &file_data[2..2 + name_len];
    let app_name = std::str::from_utf8(name_bytes).unwrap_or("FixtureGen");
    runner.dispatcher_mut().set_launched_app_path(app_name);

    let rsrc_data = &file_data[rsrc_start..rsrc_start + rsrc_len];
    let fork = ResourceFork::parse(rsrc_data).ok_or("Failed to parse resource fork")?;
    runner
        .load_app(&fork)
        .ok_or_else(|| "Failed to load app".to_string())
}

fn load_disk_image(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
    let image = crate::disk_image::extract_dc42_or_hfs(file_data)?
        .ok_or_else(|| "Not a DC42/raw HFS disk image".to_string())?;

    let mut executable_entry: Option<ExecutableCandidate> = None;
    insert_payload_into_vfs(
        runner,
        payload_from_disk_image(image)?,
        &mut executable_entry,
    );
    log_vfs(runner);

    let executable = executable_entry.ok_or("No executable found in disk image")?;
    if crate::runner::trace_load_enabled() {
        eprintln!("[LOAD] Selected executable: {}", executable.name);
    }
    runner
        .dispatcher_mut()
        .set_launched_app_path(&executable.name);

    let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
    runner
        .load_app(&fork)
        .ok_or_else(|| "Failed to load app".to_string())
}

fn load_web_pack(runner: &mut FixtureRunner, file_data: &[u8]) -> Result<LoadedApp, String> {
    let mut offset = WEB_PACK_MAGIC.len();
    let entry_count = read_u32_be(file_data, &mut offset)? as usize;
    let mut executable_entry: Option<ExecutableCandidate> = None;

    for _ in 0..entry_count {
        let name_len = read_u16_be(file_data, &mut offset)? as usize;
        let name_bytes = read_exact(file_data, &mut offset, name_len)?;
        let name = String::from_utf8(name_bytes.to_vec())
            .map_err(|_| "Invalid UTF-8 in web pack entry name".to_string())?;

        let file_type = read_exact(file_data, &mut offset, 4)?;
        let mut file_type_code = [0u8; 4];
        file_type_code.copy_from_slice(file_type);
        let is_appl = file_type_code == *b"APPL";

        let data_len = read_u32_be(file_data, &mut offset)? as usize;
        let data = read_exact(file_data, &mut offset, data_len)?.to_vec();

        let rsrc_len = read_u32_be(file_data, &mut offset)? as usize;
        let rsrc = read_exact(file_data, &mut offset, rsrc_len)?.to_vec();

        insert_forks_into_vfs(
            runner,
            &name,
            data,
            rsrc.clone(),
            file_type_code,
            *b"????",
            0,
        );
        maybe_select_executable(&mut executable_entry, &name, &rsrc, is_appl, data_len);
    }

    log_vfs(runner);

    let executable = executable_entry.ok_or("No executable found in web pack")?;
    if crate::runner::trace_load_enabled() {
        eprintln!("[LOAD] Selected executable: {}", executable.name);
    }
    runner
        .dispatcher_mut()
        .set_launched_app_path(&executable.name);

    let fork = ResourceFork::parse(&executable.rsrc).ok_or("Failed to parse resource fork")?;
    runner
        .load_app(&fork)
        .ok_or_else(|| "Failed to load app".to_string())
}

fn insert_forks_into_vfs(
    runner: &mut FixtureRunner,
    name: &str,
    data: Vec<u8>,
    rsrc: Vec<u8>,
    file_type: [u8; 4],
    creator: [u8; 4],
    finder_flags: u16,
) {
    let normalized_name = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(name);
    // If data fork is empty but resource fork doesn't parse as a resource fork,
    // use resource fork bytes as data fork (some archives have forks swapped).
    if data.is_empty() && !rsrc.is_empty() && ResourceFork::parse(&rsrc).is_none() {
        runner
            .dispatcher_mut()
            .vfs
            .insert(normalized_name.clone(), rsrc.clone());
    } else {
        runner
            .dispatcher_mut()
            .vfs
            .insert(normalized_name.clone(), data);
    }

    let data_backed_rsrc = rsrc.is_empty()
        && name.to_ascii_lowercase().ends_with(".rsrc")
        && ResourceFork::parse(
            runner
                .dispatcher()
                .vfs
                .get(&normalized_name)
                .map_or(&[][..], |bytes| bytes.as_slice()),
        )
        .is_some();

    if !rsrc.is_empty() {
        runner
            .dispatcher_mut()
            .vfs_rsrc
            .insert(normalized_name.clone(), rsrc);
    } else if data_backed_rsrc {
        let data = runner
            .dispatcher()
            .vfs
            .get(&normalized_name)
            .cloned()
            .unwrap_or_default();
        runner
            .dispatcher_mut()
            .vfs_rsrc
            .insert(normalized_name.clone(), data);
    }

    runner.dispatcher_mut().set_vfs_entry_metadata(
        &normalized_name,
        file_type,
        creator,
        finder_flags,
    );
}

#[derive(Debug)]
struct Payload {
    dirs: Vec<String>,
    files: Vec<PayloadFile>,
}

#[derive(Debug)]
struct PayloadFile {
    name: String,
    data: Vec<u8>,
    rsrc: Vec<u8>,
    file_type: [u8; 4],
    creator: [u8; 4],
    finder_flags: u16,
}

fn collect_stuffit_payload_files(archive: &SitArchive) -> Result<Vec<PayloadFile>, String> {
    let mut files = Vec::new();
    for entry in archive.entries.iter().filter(|entry| !entry.is_folder) {
        let (data, rsrc) = entry
            .decompressed_forks()
            .map_err(|e| format!("Decompress error: {:?}", e))?;
        let payload = payload_from_forks(
            &entry.name,
            data,
            rsrc,
            entry.file_type,
            entry.creator,
            entry.finder_flags,
        )?;
        files.extend(payload.files);
    }
    Ok(files)
}

fn expand_squz_payload_file(
    name: &str,
    data: &[u8],
    file_type: [u8; 4],
    creator: [u8; 4],
    finder_flags: u16,
) -> Result<Option<PayloadFile>, String> {
    if file_type != *b"SQUZ" || creator != *b"BrSq" {
        return Ok(None);
    }

    let Some(magic_pos) = data.windows(2).position(|window| window == b"KG") else {
        return Ok(None);
    };
    if magic_pos < 12 || magic_pos + 12 > data.len() {
        return Err(format!("SQUZ {name}: invalid header"));
    }
    let method = [data[magic_pos + 2], data[magic_pos + 3]];

    let mut target_type = [0u8; 4];
    target_type.copy_from_slice(&data[4..8]);
    let mut target_creator = [0u8; 4];
    target_creator.copy_from_slice(&data[8..12]);
    let header_finder_flags = u16::from_be_bytes([data[12], data[13]]);
    let target_finder_flags = if header_finder_flags != 0 {
        header_finder_flags
    } else {
        finder_flags
    };
    let resource_like_file = name.to_ascii_lowercase().ends_with(".rsrc");

    let uncompressed_len = u32::from_be_bytes([
        data[magic_pos + 4],
        data[magic_pos + 5],
        data[magic_pos + 6],
        data[magic_pos + 7],
    ]) as usize;
    let compressed_len = u32::from_be_bytes([
        data[magic_pos + 8],
        data[magic_pos + 9],
        data[magic_pos + 10],
        data[magic_pos + 11],
    ]) as usize;
    let stream_start = magic_pos + 12;
    let stream_end = stream_start
        .checked_add(compressed_len)
        .ok_or_else(|| format!("SQUZ {name}: compressed length overflow"))?;
    if stream_end > data.len() {
        return Err(format!(
            "SQUZ {name}: compressed stream truncated ({} > {})",
            stream_end,
            data.len()
        ));
    }

    let expanded = match method {
        [0x00, 0x00] => {
            if compressed_len != uncompressed_len {
                return Err(format!(
                    "SQUZ {name}: uncompressed stream length mismatch ({compressed_len} != {uncompressed_len})"
                ));
            }
            data[stream_start..stream_end].to_vec()
        }
        [0x03, 0x03] => {
            decode_broderbund_squz_0303_stream(&data[stream_start..stream_end], uncompressed_len)
                .map_err(|err| format!("SQUZ {name}: {err}"))?
        }
        [0x03, 0x04] => {
            decode_broderbund_squz_0304_stream(&data[stream_start..stream_end], uncompressed_len)
                .map_err(|err| format!("SQUZ {name}: {err}"))?
        }
        [0x03, 0x05] => {
            decode_broderbund_squz_0305_stream(&data[stream_start..stream_end], uncompressed_len)
                .map_err(|err| format!("SQUZ {name}: {err}"))?
        }
        _ => {
            if target_type == *b"APPL" {
                return Err(format!(
                    "SQUZ {name}: unsupported KG method {:02X}{:02X}",
                    method[0], method[1]
                ));
            }
            if resource_like_file {
                if crate::runner::trace_load_enabled() {
                    eprintln!(
                        "[LOAD] Mounting SQUZ \"{}\" as empty resource fork: unsupported KG method {:02X}{:02X}",
                        name, method[0], method[1]
                    );
                }
                return Ok(Some(PayloadFile {
                    name: name.to_string(),
                    data: Vec::new(),
                    rsrc: empty_resource_fork_bytes(),
                    file_type: target_type,
                    creator: target_creator,
                    finder_flags: target_finder_flags,
                }));
            }
            if crate::runner::trace_load_enabled() {
                eprintln!(
                    "[LOAD] Leaving SQUZ \"{}\" packed: unsupported KG method {:02X}{:02X}",
                    name, method[0], method[1]
                );
            }
            return Ok(None);
        }
    };
    let expanded_is_rsrc = ResourceFork::parse(&expanded).is_some();
    if target_type == *b"APPL" && !expanded_is_rsrc {
        return Err(format!(
            "SQUZ {name}: decoded application resource fork is invalid"
        ));
    }
    let rsrc = if expanded_is_rsrc {
        expanded.clone()
    } else if resource_like_file {
        if crate::runner::trace_load_enabled() {
            eprintln!(
                "[LOAD] Mounting SQUZ \"{}\" as empty resource fork: decoded payload is not a parseable resource fork",
                name
            );
        }
        empty_resource_fork_bytes()
    } else {
        Vec::new()
    };
    let data = if expanded_is_rsrc || resource_like_file {
        Vec::new()
    } else {
        expanded
    };

    if crate::runner::trace_load_enabled() {
        eprintln!(
            "[LOAD] Expanded SQUZ \"{}\" {} -> {} bytes",
            name, compressed_len, uncompressed_len
        );
    }

    Ok(Some(PayloadFile {
        name: name.to_string(),
        data,
        rsrc,
        file_type: target_type,
        creator: target_creator,
        finder_flags: target_finder_flags,
    }))
}

fn decode_broderbund_squz_0303_stream(
    stream: &[u8],
    expected_len: usize,
) -> Result<Vec<u8>, String> {
    const WINDOW_SIZE: usize = 8192;
    const LOOKAHEAD_SIZE: usize = 10;

    decode_broderbund_squz_lzss_stream(
        stream,
        expected_len,
        WINDOW_SIZE,
        LOOKAHEAD_SIZE,
        |first, second| {
            let copy_pos = (((first & 0x1F) as usize) << 8) | second as usize;
            let copy_len = ((first >> 5) as usize) + 3;
            (copy_pos, copy_len)
        },
    )
}

fn decode_broderbund_squz_0304_stream(
    stream: &[u8],
    expected_len: usize,
) -> Result<Vec<u8>, String> {
    const WINDOW_SIZE: usize = 4096;
    const LOOKAHEAD_SIZE: usize = 18;

    decode_broderbund_squz_lzss_stream(
        stream,
        expected_len,
        WINDOW_SIZE,
        LOOKAHEAD_SIZE,
        |first, second| {
            let copy_pos = (((first & 0x0F) as usize) << 8) | second as usize;
            let copy_len = ((first >> 4) as usize) + 3;
            (copy_pos, copy_len)
        },
    )
}

fn decode_broderbund_squz_0305_stream(
    stream: &[u8],
    expected_len: usize,
) -> Result<Vec<u8>, String> {
    const WINDOW_SIZE: usize = 2048;
    const LOOKAHEAD_SIZE: usize = 34;

    decode_broderbund_squz_lzss_stream(
        stream,
        expected_len,
        WINDOW_SIZE,
        LOOKAHEAD_SIZE,
        |first, second| {
            let copy_pos = (((first & 0x07) as usize) << 8) | second as usize;
            let copy_len = ((first >> 3) as usize) + 3;
            (copy_pos, copy_len)
        },
    )
}

fn decode_broderbund_squz_lzss_stream<F>(
    stream: &[u8],
    expected_len: usize,
    window_size: usize,
    lookahead_size: usize,
    decode_ref: F,
) -> Result<Vec<u8>, String>
where
    F: Fn(u8, u8) -> (usize, usize),
{
    let mut window = vec![0u8; window_size];
    let window_mask = window_size - 1;
    let mut write_pos = window_size - lookahead_size;
    let mut out = Vec::with_capacity(expected_len);
    let mut pos = 0usize;

    while pos < stream.len() && out.len() < expected_len {
        let flags = stream[pos];
        pos += 1;

        for bit in 0..8 {
            if out.len() >= expected_len {
                break;
            }

            if (flags & (1 << bit)) != 0 {
                let Some(&byte) = stream.get(pos) else {
                    return Err("literal truncated".to_string());
                };
                pos += 1;
                out.push(byte);
                window[write_pos] = byte;
                write_pos = (write_pos + 1) & window_mask;
            } else {
                if pos + 1 >= stream.len() {
                    return Err("back-reference truncated".to_string());
                }
                let first = stream[pos];
                let second = stream[pos + 1];
                pos += 2;

                let (copy_pos, copy_len) = decode_ref(first, second);
                for i in 0..copy_len {
                    if out.len() >= expected_len {
                        break;
                    }
                    let byte = window[(copy_pos + i) & window_mask];
                    out.push(byte);
                    window[write_pos] = byte;
                    write_pos = (write_pos + 1) & window_mask;
                }
            }
        }
    }

    if out.len() != expected_len {
        return Err(format!(
            "decoded {} bytes, expected {}",
            out.len(),
            expected_len
        ));
    }

    Ok(out)
}

fn empty_resource_fork_bytes() -> Vec<u8> {
    let data_offset = 16u32;
    let data_length = 0u32;
    let map_offset = 16u32;
    let map_length = 32u32;

    let mut bytes = vec![0u8; (map_offset + map_length) as usize];
    let mut header = [0u8; 16];
    header[0..4].copy_from_slice(&data_offset.to_be_bytes());
    header[4..8].copy_from_slice(&map_offset.to_be_bytes());
    header[8..12].copy_from_slice(&data_length.to_be_bytes());
    header[12..16].copy_from_slice(&map_length.to_be_bytes());
    bytes[0..16].copy_from_slice(&header);

    let map_start = map_offset as usize;
    bytes[map_start..map_start + 16].copy_from_slice(&header);
    bytes[map_start + 24..map_start + 26].copy_from_slice(&30u16.to_be_bytes());
    bytes[map_start + 26..map_start + 28].copy_from_slice(&32u16.to_be_bytes());
    bytes[map_start + 28..map_start + 30].copy_from_slice(&0xFFFFu16.to_be_bytes());

    bytes
}

fn payload_from_forks(
    name: &str,
    data: Vec<u8>,
    rsrc: Vec<u8>,
    file_type: [u8; 4],
    creator: [u8; 4],
    finder_flags: u16,
) -> Result<Payload, String> {
    if let Some(file) = expand_squz_payload_file(name, &data, file_type, creator, finder_flags)? {
        return Ok(Payload {
            dirs: Vec::new(),
            files: vec![file],
        });
    }

    if let Some(image) = crate::disk_image::extract_dc42_or_hfs(&data)
        .map_err(|e| format!("Disk image {name}: {e}"))?
    {
        if crate::runner::trace_load_enabled() {
            eprintln!(
                "[LOAD] Extracting HFS disk image \"{}\" from data fork: volume \"{}\", {} files",
                name,
                image.volume_name,
                image.files.len()
            );
        }
        return payload_from_disk_image(image);
    }

    if let Some(image) = crate::disk_image::extract_dc42_or_hfs(&rsrc)
        .map_err(|e| format!("Disk image {name}: {e}"))?
    {
        if crate::runner::trace_load_enabled() {
            eprintln!(
                "[LOAD] Extracting HFS disk image \"{}\" from resource fork: volume \"{}\", {} files",
                name,
                image.volume_name,
                image.files.len()
            );
        }
        return payload_from_disk_image(image);
    }

    Ok(Payload {
        dirs: Vec::new(),
        files: vec![PayloadFile {
            name: name.to_string(),
            data,
            rsrc,
            file_type,
            creator,
            finder_flags,
        }],
    })
}

fn payload_from_disk_image(image: crate::disk_image::DiskImageContents) -> Result<Payload, String> {
    let mut files = Vec::new();
    for file in image.files {
        if let Some(expanded) = expand_squz_payload_file(
            &file.path,
            &file.data,
            file.file_type,
            file.creator,
            file.finder_flags,
        )? {
            files.push(expanded);
        } else {
            files.push(PayloadFile {
                name: file.path,
                data: file.data,
                rsrc: file.rsrc,
                file_type: file.file_type,
                creator: file.creator,
                finder_flags: file.finder_flags,
            });
        }
    }
    Ok(Payload {
        dirs: image.dirs,
        files,
    })
}

fn insert_payload_into_vfs(
    runner: &mut FixtureRunner,
    payload: Payload,
    executable_entry: &mut Option<ExecutableCandidate>,
) {
    for dir in payload.dirs {
        let normalized = crate::trap::dispatch::TrapDispatcher::normalize_vfs_path(&dir);
        runner.dispatcher_mut().ensure_vfs_directory(&normalized);
    }

    for file in payload.files {
        let data_len = file.data.len();
        let is_appl = file.file_type == *b"APPL";
        let rsrc = file.rsrc;
        insert_forks_into_vfs(
            runner,
            &file.name,
            file.data,
            rsrc.clone(),
            file.file_type,
            file.creator,
            file.finder_flags,
        );
        maybe_select_executable(executable_entry, &file.name, &rsrc, is_appl, data_len);
    }
}

fn maybe_select_executable(
    executable_entry: &mut Option<ExecutableCandidate>,
    name: &str,
    rsrc: &[u8],
    is_appl: bool,
    data_len: usize,
) {
    if rsrc.is_empty() {
        return;
    }

    let is_executable = if let Some(fork) = ResourceFork::parse(rsrc) {
        fork.get_code(0).is_some()
    } else {
        false
    };
    if !is_executable {
        return;
    }

    // SYSTEMLESS_LOAD_EXECUTABLE: case-sensitive substring match against the
    // archive entry name. When the env var is set and a candidate matches
    // it wins outright over the size/APPL heuristic — needed for archives
    // that contain multiple bootable executables (e.g. Prince of
    // Destruction ships both "MARS™ Player" and a larger "MARS™ Master
    // (sw)" authoring variant; the heuristic picks the larger Master,
    // but the user-facing runtime is Player).
    let override_match = executable_name_override()
        .map(|needle| name.contains(needle.as_str()))
        .unwrap_or(false);
    let prev_override_match = match (executable_entry.as_ref(), executable_name_override()) {
        (Some(prev), Some(needle)) => prev.name.contains(needle.as_str()),
        _ => false,
    };

    let candidate = ExecutableCandidate {
        name: name.to_string(),
        rsrc: rsrc.to_vec(),
        is_appl,
        has_data_fork: data_len > 0,
        score: data_len.max(rsrc.len()),
    };

    let take = if override_match && !prev_override_match {
        true
    } else if !override_match && prev_override_match {
        false
    } else {
        match executable_entry.as_ref() {
            Some(prev) => candidate.selection_key() > prev.selection_key(),
            None => true,
        }
    };

    if take {
        *executable_entry = Some(candidate);
    }
}

fn executable_name_override() -> Option<String> {
    std::env::var("SYSTEMLESS_LOAD_EXECUTABLE")
        .ok()
        .filter(|s| !s.is_empty())
}

#[derive(Clone, Debug)]
struct ExecutableCandidate {
    name: String,
    rsrc: Vec<u8>,
    is_appl: bool,
    has_data_fork: bool,
    score: usize,
}

impl ExecutableCandidate {
    fn selection_key(&self) -> (bool, bool, usize) {
        (self.is_appl, self.has_data_fork, self.score)
    }
}

fn is_stuffit_archive(bytes: &[u8]) -> bool {
    bytes.len() >= 80 && (&bytes[0..4] == b"SIT!" || &bytes[0..7] == b"StuffIt")
}

fn log_vfs(runner: &FixtureRunner) {
    if !crate::runner::trace_load_enabled() {
        return;
    }
    eprintln!("[VFS] Data fork entries:");
    for key in runner.dispatcher().vfs.keys() {
        let size = runner
            .dispatcher()
            .vfs
            .get(key)
            .map(|v| v.len())
            .unwrap_or(0);
        eprintln!("  \"{}\" ({} bytes)", key, size);
    }
    eprintln!("[VFS] Resource fork entries:");
    for key in runner.dispatcher().vfs_rsrc.keys() {
        let size = runner
            .dispatcher()
            .vfs_rsrc
            .get(key)
            .map(|v| v.len())
            .unwrap_or(0);
        eprintln!("  \"{}\" ({} bytes)", key, size);
    }
}
fn read_exact<'a>(buf: &'a [u8], offset: &mut usize, len: usize) -> Result<&'a [u8], String> {
    let end = offset
        .checked_add(len)
        .ok_or_else(|| "Web pack offset overflow".to_string())?;
    if end > buf.len() {
        return Err("Web pack truncated".to_string());
    }
    let slice = &buf[*offset..end];
    *offset = end;
    Ok(slice)
}

fn read_u16_be(buf: &[u8], offset: &mut usize) -> Result<u16, String> {
    let bytes = read_exact(buf, offset, 2)?;
    Ok(u16::from_be_bytes([bytes[0], bytes[1]]))
}

fn read_u32_be(buf: &[u8], offset: &mut usize) -> Result<u32, String> {
    let bytes = read_exact(buf, offset, 4)?;
    Ok(u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]))
}

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

    fn make_single_resource_fork_bytes(res_type: [u8; 4], res_id: i16, data: &[u8]) -> Vec<u8> {
        let data_offset = 16u32;
        let data_length = (4 + data.len()) as u32;
        let map_offset = data_offset + data_length;
        let type_list_offset = 30u16;
        let ref_list_offset = 10u16;
        let name_list_offset = 40u16;
        let map_length = 52u32;

        let mut bytes = vec![0u8; (map_offset + map_length) as usize];
        let mut header = [0u8; 16];
        header[0..4].copy_from_slice(&data_offset.to_be_bytes());
        header[4..8].copy_from_slice(&map_offset.to_be_bytes());
        header[8..12].copy_from_slice(&data_length.to_be_bytes());
        header[12..16].copy_from_slice(&map_length.to_be_bytes());
        bytes[0..16].copy_from_slice(&header);

        let data_start = data_offset as usize;
        bytes[data_start..data_start + 4].copy_from_slice(&(data.len() as u32).to_be_bytes());
        bytes[data_start + 4..data_start + 4 + data.len()].copy_from_slice(data);

        let map_start = map_offset as usize;
        bytes[map_start..map_start + 16].copy_from_slice(&header);
        bytes[map_start + 24..map_start + 26].copy_from_slice(&type_list_offset.to_be_bytes());
        bytes[map_start + 26..map_start + 28].copy_from_slice(&name_list_offset.to_be_bytes());

        let type_list_start = map_start + type_list_offset as usize;
        bytes[type_list_start..type_list_start + 2].copy_from_slice(&0u16.to_be_bytes());
        bytes[type_list_start + 2..type_list_start + 6].copy_from_slice(&res_type);
        bytes[type_list_start + 6..type_list_start + 8].copy_from_slice(&0u16.to_be_bytes());
        bytes[type_list_start + 8..type_list_start + 10]
            .copy_from_slice(&ref_list_offset.to_be_bytes());

        let ref_list_start = map_start + type_list_offset as usize + ref_list_offset as usize;
        bytes[ref_list_start..ref_list_start + 2].copy_from_slice(&(res_id as u16).to_be_bytes());
        bytes[ref_list_start + 2..ref_list_start + 4].copy_from_slice(&0xFFFFu16.to_be_bytes());
        bytes[ref_list_start + 5..ref_list_start + 8].copy_from_slice(&0u32.to_be_bytes()[1..4]);

        bytes
    }

    #[test]
    fn executable_selection_prefers_real_data_fork_app_over_larger_manual() {
        let manual_rsrc = make_single_resource_fork_bytes(*b"CODE", 0, &[0; 1024]);
        let app_rsrc = make_single_resource_fork_bytes(*b"CODE", 0, &[0; 128]);
        let mut selected = None;

        maybe_select_executable(
            &mut selected,
            "Marathon/Marathon Manual",
            &manual_rsrc,
            true,
            0,
        );
        maybe_select_executable(
            &mut selected,
            "Marathon/Marathon v1.2",
            &app_rsrc,
            true,
            322_352,
        );

        let selected = selected.expect("expected an executable candidate");
        assert_eq!(selected.name, "Marathon/Marathon v1.2");
    }

    #[test]
    fn broderbund_squz_0304_decodes_literals_and_backrefs() {
        let stream = [
            0xFF, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', 0x00, 0xFF, 0xEE,
        ];

        let decoded = decode_broderbund_squz_0304_stream(&stream, 26).unwrap();
        assert_eq!(decoded, b"ABCDEFGHABCDEFGHABCDEFGHAB");
    }

    #[test]
    fn broderbund_squz_0305_uses_2k_window_and_5_bit_lengths() {
        let stream = [
            0xFF, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', 0x00, 0x2F, 0xDE, 0x2F, 0xDE,
            0x2F, 0xDE,
        ];

        let decoded = decode_broderbund_squz_0305_stream(&stream, 26).unwrap();
        assert_eq!(decoded, b"ABCDEFGHABCDEFGHABCDEFGHAB");
    }

    #[test]
    fn broderbund_squz_0303_uses_8k_window_and_13_bit_offsets() {
        let stream = [
            0x4E, 0x1F, 0xF5, 0x02, 0x00, 0x01, 0x1F, 0xFA, 0x7F, 0xF3, 0x01,
        ];

        let decoded = decode_broderbund_squz_0303_stream(&stream, 16).unwrap();
        assert_eq!(decoded, [0, 0, 0, 2, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1]);
    }

    #[test]
    fn broderbund_squz_uncompressed_payload_becomes_resource_fork() {
        let rsrc = make_single_resource_fork_bytes(*b"TEST", 128, b"payload");
        let mut file = Vec::new();
        file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
        file.extend_from_slice(b"PLR1");
        file.extend_from_slice(b"PLRM");
        file.extend_from_slice(&0x0500u16.to_be_bytes());
        file.extend_from_slice(&[0; 42]);
        file.extend_from_slice(b"KG\0\0");
        file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
        file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
        file.extend_from_slice(&rsrc);

        let expanded = expand_squz_payload_file("AllSounds1.rsrc", &file, *b"SQUZ", *b"BrSq", 0)
            .unwrap()
            .unwrap();

        assert!(expanded.data.is_empty());
        assert_eq!(expanded.rsrc, rsrc);
        assert_eq!(expanded.file_type, *b"PLR1");
        assert_eq!(expanded.creator, *b"PLRM");
        assert_eq!(expanded.finder_flags, 0x0500);
        assert!(ResourceFork::parse(&expanded.rsrc).is_some());
    }

    #[test]
    fn broderbund_squz_0305_resource_payload_becomes_resource_fork() {
        let rsrc = make_single_resource_fork_bytes(*b"TEST", 128, b"payload");
        let mut stream = Vec::new();
        for chunk in rsrc.chunks(8) {
            stream.push(((1u16 << chunk.len()) - 1) as u8);
            stream.extend_from_slice(chunk);
        }

        let mut file = Vec::new();
        file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
        file.extend_from_slice(b"PLR2");
        file.extend_from_slice(b"PLRM");
        file.extend_from_slice(&0x0500u16.to_be_bytes());
        file.extend_from_slice(&[0; 42]);
        file.extend_from_slice(b"KG\x03\x05");
        file.extend_from_slice(&(rsrc.len() as u32).to_be_bytes());
        file.extend_from_slice(&(stream.len() as u32).to_be_bytes());
        file.extend_from_slice(&stream);

        let expanded = expand_squz_payload_file("ABCBook.rsrc", &file, *b"SQUZ", *b"BrSq", 0)
            .unwrap()
            .unwrap();

        assert!(expanded.data.is_empty());
        assert_eq!(expanded.rsrc, rsrc);
        assert_eq!(expanded.file_type, *b"PLR2");
        assert_eq!(expanded.creator, *b"PLRM");
        assert_eq!(expanded.finder_flags, 0x0500);
        assert!(ResourceFork::parse(&expanded.rsrc).is_some());
    }

    #[test]
    fn broderbund_squz_compressed_non_resource_payload_stays_data_fork() {
        let stream = [0xFF, b'H', b'e', b'l', b'l', b'o'];
        let mut file = Vec::new();
        file.extend_from_slice(&5u32.to_be_bytes());
        file.extend_from_slice(b"TEXT");
        file.extend_from_slice(b"PLRM");
        file.extend_from_slice(&0x0100u16.to_be_bytes());
        file.extend_from_slice(&[0; 42]);
        file.extend_from_slice(b"KG\x03\x03");
        file.extend_from_slice(&5u32.to_be_bytes());
        file.extend_from_slice(&(stream.len() as u32).to_be_bytes());
        file.extend_from_slice(&stream);

        let expanded = expand_squz_payload_file("PR Scrapbook", &file, *b"SQUZ", *b"BrSq", 0)
            .unwrap()
            .unwrap();

        assert_eq!(expanded.data, b"Hello");
        assert!(expanded.rsrc.is_empty());
        assert_eq!(expanded.file_type, *b"TEXT");
        assert_eq!(expanded.creator, *b"PLRM");
        assert_eq!(expanded.finder_flags, 0x0100);
    }

    #[test]
    fn empty_resource_fork_is_parseable() {
        assert!(ResourceFork::parse(&empty_resource_fork_bytes()).is_some());
    }

    #[test]
    fn broderbund_squz_unparseable_rsrc_payload_mounts_empty_resource_fork() {
        let stream = [0xFF, b'H', b'e', b'l', b'l', b'o'];
        let mut file = Vec::new();
        file.extend_from_slice(&5u32.to_be_bytes());
        file.extend_from_slice(b"PLR2");
        file.extend_from_slice(b"PLRM");
        file.extend_from_slice(&0x0500u16.to_be_bytes());
        file.extend_from_slice(&[0; 42]);
        file.extend_from_slice(b"KG\x03\x03");
        file.extend_from_slice(&5u32.to_be_bytes());
        file.extend_from_slice(&(stream.len() as u32).to_be_bytes());
        file.extend_from_slice(&stream);

        let expanded = expand_squz_payload_file("ABCBook.rsrc", &file, *b"SQUZ", *b"BrSq", 0)
            .unwrap()
            .unwrap();

        assert!(expanded.data.is_empty());
        assert!(ResourceFork::parse(&expanded.rsrc).is_some());
    }
}
