use std::io::{Result, Write};

use object::{Endianness, Object, ObjectComdat, ObjectSection, ObjectSymbol};
use object::read::archive::ArchiveFile;
use object::read::macho::{DyldCache, FatArch, FatHeader};

use std::{env, fs, io, process};

fn main() {
    let mut args = env::args();
    let cmd = args.next().unwrap();
    if args.len() == 0 {
        eprintln!("Usage: {} <file> [<member>...]", cmd);
        process::exit(1);
    }
    let file_path = args.next().unwrap();
    let member_names: Vec<_> = args.collect();

    let file = match fs::File::open(&file_path) {
        Ok(file) => file,
        Err(err) => {
            eprintln!("Failed to open file '{}': {}", file_path, err,);
            process::exit(1);
        }
    };
    let extra_files = open_subcaches_if_exist(&file_path);
    let file = match unsafe { memmap2::Mmap::map(&file) } {
        Ok(mmap) => mmap,
        Err(err) => {
            eprintln!("Failed to map file '{}': {}", file_path, err,);
            process::exit(1);
        }
    };
    let extra_files: Vec<_> = extra_files
        .into_iter()
        .map(
            |subcache_file| match unsafe { memmap2::Mmap::map(&subcache_file) } {
                Ok(mmap) => mmap,
                Err(err) => {
                    eprintln!("Failed to map file '{}': {}", file_path, err,);
                    process::exit(1);
                }
            },
        )
        .collect();
    let extra_file_data: Vec<&[u8]> = extra_files.iter().map(|f| &**f).collect();

    let stdout = io::stdout();
    let stderr = io::stderr();
    print(
        &mut stdout.lock(),
        &mut stderr.lock(),
        &*file,
        &extra_file_data,
        member_names,
    )
        .unwrap();
}

// If the file is a dyld shared cache, and we're on macOS 12 or later,
// then there will be one or more "subcache" files next to this file,
// with the names filename.1, filename.2 etc.
// Read those files now, if they exist, even if we don't know that
// we're dealing with a dyld shared cache. By the time we know what
// we're dealing with, it's too late to read more files.
fn open_subcaches_if_exist(path: &str) -> Vec<fs::File> {
    let mut files = Vec::new();
    for i in 1.. {
        let subcache_path = format!("{}.{}", path, i);
        match fs::File::open(&subcache_path) {
            Ok(subcache_file) => files.push(subcache_file),
            Err(_) => break,
        };
    }
    let symbols_subcache_path = format!("{}.symbols", path);
    if let Ok(subcache_file) = fs::File::open(&symbols_subcache_path) {
        files.push(subcache_file);
    };
    files
}

pub fn print<W: Write, E: Write>(
    w: &mut W,
    e: &mut E,
    file: &[u8],
    extra_files: &[&[u8]],
    member_names: Vec<String>,
) -> Result<()> {
    let mut member_names: Vec<_> = member_names.into_iter().map(|name| (name, false)).collect();

    if let Ok(archive) = ArchiveFile::parse(file) {
        writeln!(w, "Format: Archive (kind: {:?})", archive.kind())?;
        for member in archive.members() {
            match member {
                Ok(member) => {
                    if find_member(&mut member_names, member.name()) {
                        writeln!(w)?;
                        writeln!(w, "{}:", String::from_utf8_lossy(member.name()))?;
                        if let Ok(data) = member.data(file) {
                            dump_object(w, e, data)?;
                        }
                    }
                }
                Err(err) => writeln!(e, "Failed to parse archive member: {}", err)?,
            }
        }
    } else if let Ok(arches) = FatHeader::parse_arch32(file) {
        writeln!(w, "Format: Mach-O Fat 32")?;
        for arch in arches {
            writeln!(w)?;
            writeln!(w, "Fat Arch: {:?}", arch.architecture())?;
            match arch.data(file) {
                Ok(data) => dump_object(w, e, data)?,
                Err(err) => writeln!(e, "Failed to parse Fat 32 data: {}", err)?,
            }
        }
    } else if let Ok(arches) = FatHeader::parse_arch64(file) {
        writeln!(w, "Format: Mach-O Fat 64")?;
        for arch in arches {
            writeln!(w)?;
            writeln!(w, "Fat Arch: {:?}", arch.architecture())?;
            match arch.data(file) {
                Ok(data) => dump_object(w, e, data)?,
                Err(err) => writeln!(e, "Failed to parse Fat 64 data: {}", err)?,
            }
        }
    } else if let Ok(cache) = DyldCache::<Endianness>::parse(file, extra_files) {
        writeln!(w, "Format: dyld cache {:?}-endian", cache.endianness())?;
        writeln!(w, "Architecture: {:?}", cache.architecture())?;
        for image in cache.images() {
            let path = match image.path() {
                Ok(path) => path,
                Err(err) => {
                    writeln!(e, "Failed to parse dydld image name: {}", err)?;
                    continue;
                }
            };
            if !find_member(&mut member_names, path.as_bytes()) {
                continue;
            }
            writeln!(w)?;
            writeln!(w, "{}:", path)?;
            let file = match image.parse_object() {
                Ok(file) => file,
                Err(err) => {
                    writeln!(e, "Failed to parse file: {}", err)?;
                    continue;
                }
            };
            dump_parsed_object(w, e, &file)?;
        }
    } else {
        dump_object(w, e, file)?;
    }

    for (name, found) in member_names {
        if !found {
            writeln!(e, "Failed to find member '{}", name)?;
        }
    }
    Ok(())
}

fn find_member(member_names: &mut [(String, bool)], name: &[u8]) -> bool {
    if member_names.is_empty() {
        return true;
    }
    match member_names.iter().position(|x| x.0.as_bytes() == name) {
        Some(i) => {
            member_names[i].1 = true;
            true
        }
        None => false,
    }
}

fn dump_object<W: Write, E: Write>(w: &mut W, e: &mut E, data: &[u8]) -> Result<()> {
    match object::File::parse(data) {
        Ok(file) => {
            dump_parsed_object(w, e, &file)?;
        }
        Err(err) => {
            writeln!(e, "Failed to parse file: {}", err)?;
        }
    }
    Ok(())
}

fn dump_parsed_object<W: Write, E: Write>(w: &mut W, e: &mut E, file: &object::File) -> Result<()> {
    writeln!(
        w,
        "Format: {:?} {:?}-endian {}-bit",
        file.format(),
        file.endianness(),
        if file.is_64() { "64" } else { "32" }
    )?;
    writeln!(w, "Kind: {:?}", file.kind())?;
    writeln!(w, "Architecture: {:?}", file.architecture())?;
    writeln!(w, "Flags: {:x?}", file.flags())?;
    writeln!(
        w,
        "Relative Address Base: {:x?}",
        file.relative_address_base()
    )?;
    writeln!(w, "Entry Address: {:x?}", file.entry())?;

    match file.mach_uuid() {
        Ok(Some(uuid)) => writeln!(w, "Mach UUID: {:x?}", uuid)?,
        Ok(None) => {}
        Err(err) => writeln!(e, "Failed to parse Mach UUID: {}", err)?,
    }
    match file.build_id() {
        Ok(Some(build_id)) => writeln!(w, "Build ID: {:x?}", build_id)?,
        Ok(None) => {}
        Err(err) => writeln!(e, "Failed to parse build ID: {}", err)?,
    }
    match file.gnu_debuglink() {
        Ok(Some((filename, crc))) => writeln!(
            w,
            "GNU debug link: {} CRC: {:08x}",
            String::from_utf8_lossy(filename),
            crc,
        )?,
        Ok(None) => {}
        Err(err) => writeln!(e, "Failed to parse GNU debug link: {}", err)?,
    }
    match file.gnu_debugaltlink() {
        Ok(Some((filename, build_id))) => writeln!(
            w,
            "GNU debug alt link: {}, build ID: {:x?}",
            String::from_utf8_lossy(filename),
            build_id,
        )?,
        Ok(None) => {}
        Err(err) => writeln!(e, "Failed to parse GNU debug alt link: {}", err)?,
    }
    match file.pdb_info() {
        Ok(Some(info)) => writeln!(
            w,
            "PDB file: {}, GUID: {:x?}, Age: {}",
            String::from_utf8_lossy(info.path()),
            info.guid(),
            info.age()
        )?,
        Ok(None) => {}
        Err(err) => writeln!(e, "Failed to parse PE CodeView info: {}", err)?,
    }

    for segment in file.segments() {
        writeln!(w, "{:x?}", segment)?;
    }

    for section in file.sections() {
        writeln!(w, "{}: {:x?}", section.index().0, section)?;
    }

    for comdat in file.comdats() {
        write!(w, "{:?} Sections:", comdat)?;
        for section in comdat.sections() {
            write!(w, " {}", section.0)?;
        }
        writeln!(w)?;
    }

    writeln!(w)?;
    writeln!(w, "Symbols")?;
    for symbol in file.symbols() {
        writeln!(w, "{}: {:x?}", symbol.index().0, symbol)?;
    }

    for section in file.sections() {
        if section.relocations().next().is_some() {
            writeln!(
                w,
                "\n{} relocations",
                section.name().unwrap_or("<invalid name>")
            )?;
            for relocation in section.relocations() {
                writeln!(w, "{:x?}", relocation)?;
            }
        }
    }

    writeln!(w)?;
    writeln!(w, "Dynamic symbols")?;
    for symbol in file.dynamic_symbols() {
        writeln!(w, "{}: {:x?}", symbol.index().0, symbol)?;
    }

    if let Some(relocations) = file.dynamic_relocations() {
        writeln!(w)?;
        writeln!(w, "Dynamic relocations")?;
        for relocation in relocations {
            writeln!(w, "{:x?}", relocation)?;
        }
    }

    match file.imports() {
        Ok(imports) => {
            if !imports.is_empty() {
                writeln!(w)?;
                for import in imports {
                    writeln!(w, "{:x?}", import)?;
                }
            }
        }
        Err(err) => writeln!(e, "Failed to parse imports: {}", err)?,
    }

    match file.exports() {
        Ok(exports) => {
            if !exports.is_empty() {
                writeln!(w)?;
                for export in exports {
                    writeln!(w, "{:x?}", export)?;
                }
            }
        }
        Err(err) => writeln!(e, "Failed to parse exports: {}", err)?,
    }

    Ok(())
}