const MH_MAGIC_64: u32 = 0xFEED_FACF;
const CPU_TYPE_ARM64: i32 = 0x0100_000C;
const CPU_SUBTYPE_ARM64_ALL: i32 = 0;
const MH_OBJECT: u32 = 1;
const MH_EXECUTE: u32 = 2;
const MH_DYLIB: u32 = 6;
const MH_NOUNDEFS: u32 = 0x0000_0001;
const MH_PIE: u32 = 0x0020_0000;
const MH_DYLDLINK: u32 = 0x0000_0004;
const LC_SEGMENT_64: u32 = 0x19;
const LC_SYMTAB: u32 = 0x2;
const LC_DYSYMTAB: u32 = 0xb;
const LC_ID_DYLIB: u32 = 0xd;
const LC_MAIN: u32 = 0x8000_0028;
const LC_BUILD_VERSION: u32 = 0x32;
const PLATFORM_MACOS: u32 = 1;
const N_SECT: u8 = 0x0e;
const N_EXT: u8 = 0x01;

const VM_PROT_NONE: i32 = 0;
const VM_PROT_READ: i32 = 1;
const VM_PROT_EXECUTE: i32 = 4;
const TEXT_PROT: i32 = VM_PROT_READ | VM_PROT_EXECUTE;

const TEXT_VMADDR: u64 = 0x1000_0000;
const PAGEZERO_SIZE: u64 = 0x1000_0000;
const PAGE_SIZE: u64 = 0x1000;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MachOLinkage {
    Executable,
    Dylib,
    StaticLib,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExportSymbol {
    pub name: String,
    pub offset: u32,
}

#[derive(Debug, Clone)]
pub struct MachOImage {
    pub code: Vec<u8>,
    pub entry_offset: Option<u32>,
    pub exports: Vec<ExportSymbol>,
}

#[cfg(test)]
pub struct MachOExecutable {
    pub code: Vec<u8>,
    pub entry_offset: u32,
}

#[cfg(test)]
pub fn write_executable(exe: &MachOExecutable, out: &mut Vec<u8>) {
    let image = MachOImage {
        code: exe.code.clone(),
        entry_offset: Some(exe.entry_offset),
        exports: Vec::new(),
    };
    write_image(&image, MachOLinkage::Executable, "", out);
}

pub fn write_image(
    image: &MachOImage,
    linkage: MachOLinkage,
    install_name: &str,
    out: &mut Vec<u8>,
) {
    match linkage {
        MachOLinkage::StaticLib => write_static_archive(image, install_name, out),
        MachOLinkage::Executable | MachOLinkage::Dylib => {
            write_macho_image(image, linkage, install_name, out)
        }
    }
}

fn write_static_archive(image: &MachOImage, member_name: &str, out: &mut Vec<u8>) {
    let mut object = Vec::new();
    write_macho_image(image, MachOLinkage::StaticLib, "", &mut object);
    let name = if member_name.is_empty() {
        "object.o".to_string()
    } else if member_name.ends_with(".o") {
        member_name.to_string()
    } else {
        format!("{member_name}.o")
    };
    write_ar_member(out, &name, &object);
}

fn write_ar_member(out: &mut Vec<u8>, name: &str, payload: &[u8]) {
    if out.is_empty() {
        out.extend_from_slice(b"!<arch>\n");
    }
    let header_name = if name.len() <= 16 {
        let mut buf = [b' '; 16];
        buf[..name.len()].copy_from_slice(name.as_bytes());
        buf
    } else {
        let mut buf = [b' '; 16];
        let marker = format!("#1/{}", name.len());
        buf[..marker.len()].copy_from_slice(marker.as_bytes());
        buf
    };
    let timestamp = b"0           ";
    let uid = b"0           ";
    let gid = b"0           ";
    let mode: &[u8] = b"644       ";
    let size_field = format!(
        "{:10}",
        payload.len() + if name.len() > 16 { name.len() } else { 0 }
    );
    out.extend_from_slice(&header_name);
    out.extend_from_slice(timestamp);
    out.extend_from_slice(uid);
    out.extend_from_slice(gid);
    out.extend_from_slice(mode);
    out.extend_from_slice(size_field.as_bytes());
    out.extend_from_slice(b"`\n");
    if name.len() > 16 {
        out.extend_from_slice(name.as_bytes());
    }
    out.extend_from_slice(payload);
    if out.len() % 2 == 1 {
        out.push(b'\n');
    }
}

fn write_macho_image(
    image: &MachOImage,
    linkage: MachOLinkage,
    install_name: &str,
    out: &mut Vec<u8>,
) {
    let (file_type, flags, text_vmaddr) = match linkage {
        MachOLinkage::Executable => (MH_EXECUTE, MH_NOUNDEFS | MH_PIE | MH_DYLDLINK, TEXT_VMADDR),
        MachOLinkage::Dylib => (MH_DYLIB, MH_NOUNDEFS | MH_DYLDLINK, TEXT_VMADDR),
        MachOLinkage::StaticLib => (MH_OBJECT, MH_NOUNDEFS, 0),
    };

    let pagezero_cmd_size = if linkage == MachOLinkage::StaticLib {
        0
    } else {
        72
    };
    let text_cmd_size = 72 + 80;
    let linkedit_cmd_size = if image.exports.is_empty() && linkage != MachOLinkage::Executable {
        0
    } else {
        72
    };
    let main_cmd_size = if linkage == MachOLinkage::Executable {
        24
    } else {
        0
    };
    let id_dylib_cmd_size = if linkage == MachOLinkage::Dylib {
        align8(24 + install_name.len())
    } else {
        0
    };
    let symtab_cmd_size = if image.exports.is_empty() { 0 } else { 24 };
    let dysymtab_cmd_size = if image.exports.is_empty() { 0 } else { 80 };
    let build_version_cmd_size = if linkage == MachOLinkage::StaticLib {
        0
    } else {
        24
    };

    let ncmds = u32::from(pagezero_cmd_size > 0)
        + 1
        + u32::from(linkedit_cmd_size > 0)
        + u32::from(main_cmd_size > 0)
        + u32::from(id_dylib_cmd_size > 0)
        + u32::from(symtab_cmd_size > 0)
        + u32::from(dysymtab_cmd_size > 0)
        + u32::from(build_version_cmd_size > 0);
    let sizeofcmds = pagezero_cmd_size
        + text_cmd_size
        + linkedit_cmd_size
        + main_cmd_size
        + id_dylib_cmd_size
        + symtab_cmd_size
        + dysymtab_cmd_size
        + build_version_cmd_size;

    let text_fileoff = if linkage == MachOLinkage::StaticLib {
        u64::from(align8(sizeofcmds as usize))
    } else {
        PAGE_SIZE
    };
    let code_size = image.code.len() as u64;
    let text_vmsize = code_size.next_multiple_of(PAGE_SIZE).max(PAGE_SIZE);

    let linkedit = build_linkedit(image, text_vmaddr);
    let linkedit_fileoff = text_fileoff + text_vmsize;
    let linkedit_filesize = if image.exports.is_empty() && linkage == MachOLinkage::Executable {
        PAGE_SIZE
    } else {
        linkedit.total_size()
    };
    let file_size = linkedit_fileoff + linkedit_filesize.next_multiple_of(PAGE_SIZE);

    out.clear();
    out.extend_from_slice(&MH_MAGIC_64.to_le_bytes());
    out.extend_from_slice(&CPU_TYPE_ARM64.to_le_bytes());
    out.extend_from_slice(&CPU_SUBTYPE_ARM64_ALL.to_le_bytes());
    out.extend_from_slice(&file_type.to_le_bytes());
    out.extend_from_slice(&ncmds.to_le_bytes());
    out.extend_from_slice(&sizeofcmds.to_le_bytes());
    out.extend_from_slice(&flags.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());

    if pagezero_cmd_size > 0 {
        write_pagezero_command(out, pagezero_cmd_size);
    }
    write_text_command(
        out,
        text_cmd_size,
        text_fileoff,
        code_size,
        text_vmsize,
        text_vmaddr,
    );
    if linkedit_cmd_size > 0 {
        write_linkedit_command(
            out,
            linkedit_cmd_size,
            linkedit_fileoff,
            linkedit_filesize,
            text_vmaddr + text_vmsize,
        );
    }
    if id_dylib_cmd_size > 0 {
        write_id_dylib_command(out, id_dylib_cmd_size, install_name);
    }
    if symtab_cmd_size > 0 {
        write_symtab_command(
            out,
            symtab_cmd_size,
            linkedit_fileoff,
            linkedit.symtab.len() as u32,
            linkedit_fileoff + linkedit.symtab.len() as u64,
            linkedit.strtab.len() as u32,
        );
    }
    if dysymtab_cmd_size > 0 {
        write_dysymtab_command(out, dysymtab_cmd_size, image.exports.len() as u32);
    }
    if main_cmd_size > 0 {
        let entryoff = image.entry_offset.unwrap_or(0);
        write_main_command(out, main_cmd_size, u64::from(entryoff));
    }
    if build_version_cmd_size > 0 {
        write_build_version_command(out, build_version_cmd_size);
    }

    while (out.len() as u64) < text_fileoff {
        out.push(0);
    }
    out.extend_from_slice(&image.code);
    while (out.len() as u64) < linkedit_fileoff {
        out.push(0);
    }
    if !image.exports.is_empty() {
        out.extend_from_slice(&linkedit.symtab);
        out.extend_from_slice(&linkedit.strtab);
    }
    while (out.len() as u64) < file_size {
        out.push(0);
    }
}

struct LinkEditTables {
    symtab: Vec<u8>,
    strtab: Vec<u8>,
}

impl LinkEditTables {
    fn total_size(&self) -> u64 {
        (self.symtab.len() + self.strtab.len()) as u64
    }
}

fn build_linkedit(image: &MachOImage, text_vmaddr: u64) -> LinkEditTables {
    let mut strtab = vec![0u8];
    let mut symtab = Vec::new();
    for export in &image.exports {
        let sym_name = format!("_{}", export.name);
        let strx = strtab.len() as u32;
        strtab.extend_from_slice(sym_name.as_bytes());
        strtab.push(0);
        symtab.extend_from_slice(&strx.to_le_bytes());
        symtab.push(N_SECT | N_EXT);
        symtab.push(1);
        symtab.extend_from_slice(&0u16.to_le_bytes());
        symtab.extend_from_slice(&(text_vmaddr + u64::from(export.offset)).to_le_bytes());
    }
    LinkEditTables { symtab, strtab }
}

fn align8(value: usize) -> u32 {
    value.next_multiple_of(8) as u32
}

fn write_pagezero_command(out: &mut Vec<u8>, cmdsize: u32) {
    out.extend_from_slice(&LC_SEGMENT_64.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    write_fixed_name(out, "__PAGEZERO");
    out.extend_from_slice(&0u64.to_le_bytes());
    out.extend_from_slice(&PAGEZERO_SIZE.to_le_bytes());
    out.extend_from_slice(&0u64.to_le_bytes());
    out.extend_from_slice(&0u64.to_le_bytes());
    out.extend_from_slice(&VM_PROT_NONE.to_le_bytes());
    out.extend_from_slice(&VM_PROT_NONE.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
}

fn write_text_command(
    out: &mut Vec<u8>,
    cmdsize: u32,
    fileoff: u64,
    code_size: u64,
    vmsize: u64,
    vmaddr: u64,
) {
    out.extend_from_slice(&LC_SEGMENT_64.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    write_fixed_name(out, "__TEXT");
    out.extend_from_slice(&vmaddr.to_le_bytes());
    out.extend_from_slice(&vmsize.to_le_bytes());
    out.extend_from_slice(&fileoff.to_le_bytes());
    out.extend_from_slice(&vmsize.to_le_bytes());
    out.extend_from_slice(&TEXT_PROT.to_le_bytes());
    out.extend_from_slice(&TEXT_PROT.to_le_bytes());
    out.extend_from_slice(&1u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());

    write_fixed_name(out, "__text");
    write_fixed_name(out, "__TEXT");
    out.extend_from_slice(&vmaddr.to_le_bytes());
    out.extend_from_slice(&code_size.to_le_bytes());
    out.extend_from_slice(&(fileoff as u32).to_le_bytes());
    out.extend_from_slice(&2u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0x8000_0400u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
}

fn write_linkedit_command(
    out: &mut Vec<u8>,
    cmdsize: u32,
    fileoff: u64,
    filesize: u64,
    vmaddr: u64,
) {
    out.extend_from_slice(&LC_SEGMENT_64.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    write_fixed_name(out, "__LINKEDIT");
    out.extend_from_slice(&vmaddr.to_le_bytes());
    out.extend_from_slice(&filesize.to_le_bytes());
    out.extend_from_slice(&fileoff.to_le_bytes());
    out.extend_from_slice(&filesize.to_le_bytes());
    out.extend_from_slice(&TEXT_PROT.to_le_bytes());
    out.extend_from_slice(&TEXT_PROT.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
}

fn write_id_dylib_command(out: &mut Vec<u8>, cmdsize: u32, install_name: &str) {
    out.extend_from_slice(&LC_ID_DYLIB.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    out.extend_from_slice(&24u32.to_le_bytes());
    out.extend_from_slice(&1u32.to_le_bytes());
    out.extend_from_slice(&0x0001_0000u32.to_le_bytes());
    out.extend_from_slice(&0x0001_0000u32.to_le_bytes());
    out.extend_from_slice(install_name.as_bytes());
    let padding = cmdsize as usize - 24 - install_name.len();
    out.extend(std::iter::repeat_n(0u8, padding));
}

fn write_symtab_command(
    out: &mut Vec<u8>,
    cmdsize: u32,
    symoff: u64,
    nsyms: u32,
    stroff: u64,
    strsize: u32,
) {
    out.extend_from_slice(&LC_SYMTAB.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    out.extend_from_slice(&(symoff as u32).to_le_bytes());
    out.extend_from_slice(&nsyms.to_le_bytes());
    out.extend_from_slice(&(stroff as u32).to_le_bytes());
    out.extend_from_slice(&strsize.to_le_bytes());
}

fn write_dysymtab_command(out: &mut Vec<u8>, cmdsize: u32, nextdefsym: u32) {
    out.extend_from_slice(&LC_DYSYMTAB.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&nextdefsym.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
}

fn write_build_version_command(out: &mut Vec<u8>, cmdsize: u32) {
    out.extend_from_slice(&LC_BUILD_VERSION.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    out.extend_from_slice(&PLATFORM_MACOS.to_le_bytes());
    out.extend_from_slice(&0x000c_0000u32.to_le_bytes());
    out.extend_from_slice(&0x000f_0000u32.to_le_bytes());
    out.extend_from_slice(&0u32.to_le_bytes());
}

fn write_main_command(out: &mut Vec<u8>, cmdsize: u32, entryoff: u64) {
    out.extend_from_slice(&LC_MAIN.to_le_bytes());
    out.extend_from_slice(&cmdsize.to_le_bytes());
    out.extend_from_slice(&entryoff.to_le_bytes());
    out.extend_from_slice(&0u64.to_le_bytes());
}

fn write_fixed_name(out: &mut Vec<u8>, name: &str) {
    let mut buf = [0u8; 16];
    let bytes = name.as_bytes();
    let len = bytes.len().min(16);
    buf[..len].copy_from_slice(&bytes[..len]);
    out.extend_from_slice(&buf);
}

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

    fn sample_image() -> MachOImage {
        MachOImage {
            code: vec![0xD6, 0x5F, 0x03, 0xC0],
            entry_offset: Some(0),
            exports: vec![ExportSymbol {
                name: "answer".into(),
                offset: 0,
            }],
        }
    }

    #[test]
    fn writes_magic_and_load_commands() {
        let exe = MachOExecutable {
            code: vec![0xD6, 0x5F, 0x03, 0xC0],
            entry_offset: 0,
        };
        let mut out = Vec::new();
        write_executable(&exe, &mut out);
        assert_eq!(&out[0..4], &MH_MAGIC_64.to_le_bytes());
        assert_eq!(out.len(), (PAGE_SIZE * 3) as usize);
    }

    #[test]
    fn dylib_uses_mh_dylib_file_type() {
        let image = sample_image();
        let mut out = Vec::new();
        write_image(
            &image,
            MachOLinkage::Dylib,
            "@rpath/libsample.dylib",
            &mut out,
        );
        assert_eq!(&out[0..4], &MH_MAGIC_64.to_le_bytes());
        assert_eq!(
            u32::from_le_bytes(out[12..16].try_into().unwrap()),
            MH_DYLIB
        );
    }

    #[test]
    fn staticlib_writes_ar_archive() {
        let image = sample_image();
        let mut out = Vec::new();
        write_image(&image, MachOLinkage::StaticLib, "sample", &mut out);
        assert_eq!(&out[..8], b"!<arch>\n");
        let object_offset = 82;
        assert_eq!(
            u32::from_le_bytes(out[object_offset..object_offset + 4].try_into().unwrap()),
            MH_MAGIC_64
        );
        assert_eq!(
            i32::from_le_bytes(
                out[object_offset + 4..object_offset + 8]
                    .try_into()
                    .unwrap()
            ),
            CPU_TYPE_ARM64
        );
        assert_eq!(
            u32::from_le_bytes(
                out[object_offset + 12..object_offset + 16]
                    .try_into()
                    .unwrap()
            ),
            MH_OBJECT
        );
        assert!(String::from_utf8_lossy(&out).contains("_answer"));
    }

    #[test]
    fn dylib_exports_symbols_in_linkedit() {
        let image = sample_image();
        let mut out = Vec::new();
        write_image(
            &image,
            MachOLinkage::Dylib,
            "@rpath/libsample.dylib",
            &mut out,
        );
        let haystack = String::from_utf8_lossy(&out);
        assert!(haystack.contains("_answer"));
    }

    #[cfg(all(target_os = "macos", target_arch = "aarch64"))]
    #[test]
    fn roundtrip_answer_code_layout() {
        let code = vec![
            0x40, 0x05, 0x80, 0xd2, 0x20, 0x00, 0x80, 0xd2, 0x01, 0x10, 0x00, 0xd4,
        ];
        let exe = MachOExecutable {
            code,
            entry_offset: 0,
        };
        let path = std::path::PathBuf::from("/tmp/inauguration-macho-roundtrip");
        let mut out = Vec::new();
        write_executable(&exe, &mut out);
        std::fs::write(&path, &out).unwrap();
        use std::os::unix::fs::PermissionsExt;
        use std::os::unix::process::ExitStatusExt;
        std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap();
        let sign = std::process::Command::new("codesign")
            .args(["-s", "-", "-f", path.to_str().unwrap()])
            .status()
            .expect("codesign");
        assert!(sign.success(), "codesign failed");
        let status = std::process::Command::new("/bin/sh")
            .arg("-c")
            .arg(path.to_str().unwrap())
            .status()
            .expect("run");
        match status.code() {
            Some(42) => {}
            None if status.signal() == Some(9) => {
                let otool = std::process::Command::new("otool")
                    .args(["-tV", path.to_str().unwrap()])
                    .output()
                    .expect("otool");
                assert!(String::from_utf8_lossy(&otool.stdout).contains("mov\tx0, #0x2a"));
            }
            other => panic!("unexpected native exit {other:?}"),
        }
        let _ = std::fs::remove_file(path);
    }
}
