mod utils;
use std::path::Path;
use std::env;
use lief::logging;
use lief::macho::builder::Config;
use lief::macho::Relocation;
use lief::generic::Binary as GenericBinary;
use lief::macho::binding_info::{self, AsGeneric};
use lief::macho::commands::{Command, Commands};
use lief::generic::Symbol;
use lief::Binary;

fn print_binding(binding: &binding_info::BindingInfo) {
    format!("{:?}", binding);
    match binding {
        binding_info::BindingInfo::Generic(generic) => {
            format!("{:?}", generic.symbol());
            format!("{:?}", generic.library());
            format!("{:?}", generic.segment());
        }

        binding_info::BindingInfo::Dyld(dyld) => {
            format!("{:?}", dyld.symbol());
            format!("{:?}", dyld.library());
            format!("{:?}", dyld.segment());
        }

        binding_info::BindingInfo::Chained(chained) => {
            format!("{:?}", chained.symbol());
            format!("{:?}", chained.library());
            format!("{:?}", chained.segment());
        }
        binding_info::BindingInfo::Indirect(indirect) => {
            format!("{:?}", indirect.symbol());
        }
    }
}

fn explore_macho(_: &str, macho: &lief::macho::Binary) {
    format!("{macho:?}");
    format!("{}", macho.entrypoint());
    format!("{:?}", macho.header());
    format!("{}{}", macho.header().is_32bit(), macho.header().is_64bit());
    println!("{:?}:{}:{}", macho.platform(), macho.is_ios(), macho.is_macos());

    for func in macho.functions() {
        format!("{func:?}");
    }

    for note in macho.notes() {
        format!("{note:?}");
    }

    for section in macho.sections() {
        format!("{section:?}");
        format!("{:?}", section.segment());
        for relocation in section.relocations() {
            format!("{relocation:?}");
        }
    }

    for binding in macho.bindings() {
        format!("{:?}", binding);
    }

    for stub in macho.symbol_stubs() {
        format!("{stub:?}");
        format!("{}", stub.raw().len());
    }

    for command in macho.commands() {
        format!("{command:?}");
        match command {
            Commands::Generic(gen) => {
                println!("TYPE: {:?}", gen.command_type());
            }
            Commands::DyldChainedFixups(cmd) => {
                format!("{}", cmd.payload().len());
                for binding in cmd.bindings() {
                    format!("{:?}", binding);
                }
            }
            Commands::DataInCode(cmd) => {
                format!("{}", cmd.content().len());
                for entry in cmd.entries() {
                    format!("{:?}", entry);
                }
            }
            Commands::SymbolCommand(cmd) => {
                format!("{}", cmd.symbol_table().len());
                format!("{}", cmd.string_table().len());
                format!("{}", cmd.original_str_size());
                format!("{}", cmd.original_nb_symbols());
            }
            Commands::FunctionStarts(cmd) => {
                format!("{}", cmd.content().len());
                for entry in cmd.functions() {
                    format!("{:?}", entry);
                }
            }
            Commands::DyldExportsTrie(cmd) => {
                format!("{}", cmd.content().len());
                for export in cmd.exports() {
                    format!("{:?}", export);
                    format!("{:?}", export.symbol());
                    format!("{:?}", export.alias());
                    format!("{:?}", export.alias_library());
                }
            }
            Commands::ThreadCommand(cmd) => {
                format!("{}", cmd.state().len());
            }
            Commands::CodeSignature(cmd) => {
                format!("{}", cmd.content().len());
            }
            Commands::CodeSignatureDir(cmd) => {
                format!("{}", cmd.content().len());
            }
            Commands::LinkerOptHint(cmd) => {
                format!("{}", cmd.content().len());
            }
            Commands::TwoLevelHints(cmd) => {
                format!("{}", cmd.original_nb_hints());
                format!("{}", cmd.content().len());
            }
            Commands::SegmentSplitInfo(cmd) => {
                format!("{}", cmd.content().len());
            }
            Commands::DyldInfo(cmd) => {
                format!("{}", cmd.rebase_opcodes().len());
                format!("{}", cmd.bind_opcodes().len());
                format!("{}", cmd.weak_bind_opcodes().len());
                format!("{}", cmd.lazy_bind_opcodes().len());
                format!("{}", cmd.export_trie().len());
                for binding in cmd.bindings() {
                    format!("{:?}", binding);
                    print_binding(&binding);
                }
                for export in cmd.exports() {
                    format!("{:?}", export);
                }
            }

            Commands::Note(note) => {
                println!("Note {note:?}");
            }

            Commands::FunctionVariants(func) => {
                println!("{func:?}");
                for runtime_table in func.runtime_table() {
                    println!("{runtime_table:?} {runtime_table}");
                    for entry in runtime_table.entries() {
                        println!("{entry:?} {entry}")
                    }
                }
            }

            Commands::Unknown(ukn) => {
                println!("Original: {:?}", ukn.original_command());
            }
            _ => {}
        }
    }

    for segment in macho.segments() {
        format!("{segment:?}");
        for section in segment.sections() {
            format!("{section:?}");
        }

        for relocation in segment.relocations() {
            format!("{relocation:?}");
        }

        format!("len: {}", segment.content().len());
    }

    for lib in macho.libraries() {
        format!("{lib:?}");
    }

    for reloc in macho.relocations() {
        format!("{reloc:?}");

        match reloc {
            Relocation::Dyld(_) => {
            }
            Relocation::Fixup(fixup) => {
                format!("{}", fixup.next());
            }
            Relocation::Object(_) => {
            }
            Relocation::Generic(_) => {
            }
        }
    }

    for sym in macho.symbols() {
        format!("{sym:?}");
        format!("{}", sym.demangled_name());
    }

    if let Some(info) = macho.dyld_info() {
        format!("{info:?}");
    }

    if let Some(uuid) = macho.uuid() {
        format!("{uuid:?}");
    }

    if let Some(main) = macho.main_command() {
        format!("{main:?}");
    }

    if let Some(linker) = macho.dylinker() {
        format!("{linker:?}");
    }

    if let Some(starts) = macho.function_starts() {
        format!("{starts:?}");
    }

    if let Some(version) = macho.source_version() {
        format!("{version:?}");
    }

    if let Some(cmd) = macho.thread_command() {
        format!("{cmd:?}");
    }

    if let Some(rpath) = macho.rpath() {
        format!("{rpath:?}");
    }

    if let Some(routine) = macho.routine() {
        format!("{routine:?}");
    }

    if let Some(symbol_command) = macho.symbol_command() {
        format!("{symbol_command:?}");
    }

    if let Some(dynamic_symbol) = macho.dynamic_symbol() {
        format!("{dynamic_symbol:?}");
        for sym in dynamic_symbol.indirect_symbols() {
            format!("{}", sym.name());
        }
    }

    if let Some(code_signature) = macho.code_signature() {
        format!("{code_signature:?}");
    }

    if let Some(code_signature_dir) = macho.code_signature_dir() {
        format!("{code_signature_dir:?}");
    }

    if let Some(data_in_code) = macho.data_in_code() {
        format!("{data_in_code:?}");
    }

    if let Some(segment_split_info) = macho.segment_split_info() {
        format!("{segment_split_info:?}");
    }

    if let Some(encryption_info) = macho.encryption_info() {
        format!("{encryption_info:?}");
    }

    if let Some(sub_framework) = macho.sub_framework() {
        format!("{sub_framework:?}");
    }

    for sc in macho.subclients() {
        format!("{sc:?}");
    }

    if let Some(dyld_environment) = macho.dyld_environment() {
        format!("{dyld_environment:?}");
    }

    if let Some(build_version) = macho.build_version() {
        format!("{build_version:?}");
    }

    if let Some(dyld_chained_fixups) = macho.dyld_chained_fixups() {
        format!("{dyld_chained_fixups:?}");
    }

    if let Some(dyld_exports_trie) = macho.dyld_exports_trie() {
        format!("{dyld_exports_trie:?}");
    }

    if let Some(two_level_hints) = macho.two_level_hints() {
        format!("{two_level_hints:?}");
    }

    if let Some(linker_opt_hint) = macho.linker_opt_hint() {
        format!("{linker_opt_hint:?}");
    }

    if let Some(version_min) = macho.version_min() {
        format!("{version_min:?}");
    }
}

fn test_with(bin_name: &str) {
    let path = utils::get_macho_sample(bin_name).unwrap();
    let path_str = path.to_str();
    if let Some(Binary::MachO(fat)) = Binary::parse(path_str.unwrap()) {
        for bin in fat.iter() {
            explore_macho(bin_name, &bin);
        }
    }

    // Test Read + Seek interface
    let mut file = std::fs::File::open(path).expect("Can't open the file");
    let binary = Binary::from(&mut file);
    assert!(matches!(binary, Some(Binary::MachO(_))));
}

fn test_with_str(name: &str, path_str: &str) {
    if let Some(lief::Binary::MachO(fat)) = lief::Binary::parse(path_str) {
        for bin in fat.iter() {
            explore_macho(name, &bin);
        }
    }
}

fn test_with_fullpath(name: &str, suffix: &str) {
    let path = utils::get_sample(Path::new(suffix)).unwrap();
    let path_str = path.to_str().unwrap();
    test_with_str(name, path_str);
}

#[test]
fn test_api() {
    let mut dir = env::temp_dir();
    dir.push("lief_macho_test.log");
    logging::set_path(dir.as_path());

    test_with("alivcffmpeg_armv7.dylib");
    test_with(
        "9edfb04c55289c6c682a25211a4b30b927a86fe50b014610d04d6055bd4ac23d_crypt_and_hash.macho",
    );
    test_with("MachO64_x86-64_binary_safaridriver.bin");
    test_with("FAT_MachO_x86_x86-64_library_libdyld.dylib");
    test_with("ios1-expr.bin");
    test_with("json_api.cpp_1.o");
    test_with("python3_issue_476.bin");
    test_with("FAT_MachO_x86_x86-64_library_libc++abi.dylib");
    test_with("libadd_unknown_cmd.so");
    test_with("StocksAnalytics");
    test_with("liblog_srp.dylib");
    test_with("binary.metallib");
    test_with("variants_alt.dylib");
    test_with_fullpath("CoreFoundation", "private/MachO/CoreFoundation");
}

#[test]
fn test_mut_api() {
    let path = utils::get_macho_sample("FAT_MachO_x86_x86-64_library_libc++abi.dylib").unwrap();
    let Binary::MachO(fat) = Binary::parse(path.to_str().unwrap()).unwrap() else { panic!("Expecting an ELF"); };
    for mut bin in fat.iter() {
        bin.add_library("this_is_a_dylib.dylib");
        let tmpfile = tempfile::NamedTempFile::new().unwrap();
        bin.write(tmpfile.path());
    }

    let path = utils::get_macho_sample("json_api.cpp_1.o").unwrap();
    let Binary::MachO(fat) = Binary::parse(path.to_str().unwrap()).unwrap() else { panic!("Expecting an ELF"); };
    for mut bin in fat.iter() {
        let tmpfile = tempfile::NamedTempFile::new().unwrap();
        bin.write_with_config(tmpfile.path(), Config::default());
    }
}

