//! Dynamically linked symbolic imports

// table of tuples:
// <seg-index, seg-offset, type, symbol-library-ordinal, symbol-name, addend>
// symbol flags are undocumented

use alloc::vec::Vec;
use core::fmt::{self, Debug};
use core::ops::Range;
use scroll::{Pread, Sleb128, Uleb128};

use crate::container;
use crate::error;
use crate::mach::bind_opcodes;
use crate::mach::load_command;
use crate::mach::segment;

#[derive(Debug)]
/// Import binding information generated by running the Finite State Automaton programmed via `bind_opcodes`
struct BindInformation<'a> {
    seg_index: u8,
    seg_offset: u64,
    bind_type: u8,
    symbol_library_ordinal: u8,
    symbol_name: &'a str,
    symbol_flags: u8,
    addend: i64,
    special_dylib: u8, // seeing self = 0 assuming this means the symbol is imported from itself, because its... libSystem.B.dylib?
    is_lazy: bool,
}

impl<'a> BindInformation<'a> {
    pub fn new(is_lazy: bool) -> Self {
        let mut bind_info = BindInformation::default();
        if is_lazy {
            bind_info.is_lazy = true;
            bind_info.bind_type = bind_opcodes::BIND_TYPE_POINTER;
        }
        bind_info
    }
    pub fn is_weak(&self) -> bool {
        self.symbol_flags & bind_opcodes::BIND_SYMBOL_FLAGS_WEAK_IMPORT != 0
    }
}

impl<'a> Default for BindInformation<'a> {
    fn default() -> Self {
        BindInformation {
            seg_index: 0,
            seg_offset: 0x0,
            bind_type: 0x0,
            special_dylib: 1,
            symbol_library_ordinal: 0,
            symbol_name: "",
            symbol_flags: 0,
            addend: 0,
            is_lazy: false,
        }
    }
}

#[derive(Debug)]
/// An dynamically linked symbolic import
pub struct Import<'a> {
    /// The symbol name dyld uses to resolve this import
    pub name: &'a str,
    /// The library this symbol belongs to (thanks to two-level namespaces)
    pub dylib: &'a str,
    ///  Whether the symbol is lazily resolved or not
    pub is_lazy: bool,
    /// The offset in the binary this import is found
    pub offset: u64,
    /// The size of this import
    pub size: usize,
    /// The virtual memory address at which this import is found
    pub address: u64,
    /// The addend of this import
    pub addend: i64,
    /// Whether this import is weak
    pub is_weak: bool,
    /// The offset in the stream of bind opcodes that caused this import
    pub start_of_sequence_offset: u64,
}

impl<'a> Import<'a> {
    /// Create a new import from the import binding information in `bi`
    fn new(
        bi: &BindInformation<'a>,
        libs: &[&'a str],
        segments: &[segment::Segment],
        start_of_sequence_offset: usize,
    ) -> Import<'a> {
        let (offset, address) = {
            let segment = &segments[bi.seg_index as usize];
            (
                segment.fileoff + bi.seg_offset,
                segment.vmaddr + bi.seg_offset,
            )
        };
        let size = if bi.is_lazy { 8 } else { 0 };
        Import {
            name: bi.symbol_name,
            dylib: libs[bi.symbol_library_ordinal as usize],
            is_lazy: bi.is_lazy,
            offset,
            size,
            address,
            addend: bi.addend,
            is_weak: bi.is_weak(),
            start_of_sequence_offset: start_of_sequence_offset as u64,
        }
    }
}

/// An interpreter for mach BIND opcodes.
/// Runs on prebound (non lazy) symbols (usually dylib extern consts and extern variables),
/// and lazy symbols (usually dylib functions)
pub struct BindInterpreter<'a> {
    data: &'a [u8],
    location: Range<usize>,
    lazy_location: Range<usize>,
}

impl<'a> Debug for BindInterpreter<'a> {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        fmt.debug_struct("BindInterpreter")
            .field("data", &"<... redacted ...>")
            .field(
                "location",
                &format_args!("{:#x}..{:#x}", self.location.start, self.location.end),
            )
            .field(
                "lazy_location",
                &format_args!(
                    "{:#x}..{:#x}",
                    self.lazy_location.start, self.lazy_location.end
                ),
            )
            .finish()
    }
}

impl<'a> BindInterpreter<'a> {
    /// Construct a new import binding interpreter from `bytes` and the load `command`
    pub fn new(bytes: &'a [u8], command: &load_command::DyldInfoCommand) -> Self {
        let get_pos = |off: u32, size: u32| -> Range<usize> {
            let start = off as usize;
            start..start.saturating_add(size as usize)
        };
        let location = get_pos(command.bind_off, command.bind_size);
        let lazy_location = get_pos(command.lazy_bind_off, command.lazy_bind_size);
        BindInterpreter {
            data: bytes,
            location,
            lazy_location,
        }
    }
    /// Return the imports in this binary
    pub fn imports(
        &self,
        libs: &[&'a str],
        segments: &[segment::Segment],
        ctx: container::Ctx,
    ) -> error::Result<Vec<Import<'a>>> {
        let mut imports = Vec::new();
        self.run(false, libs, segments, ctx, &mut imports)?;
        self.run(true, libs, segments, ctx, &mut imports)?;
        Ok(imports)
    }
    fn run(
        &self,
        is_lazy: bool,
        libs: &[&'a str],
        segments: &[segment::Segment],
        ctx: container::Ctx,
        imports: &mut Vec<Import<'a>>,
    ) -> error::Result<()> {
        use crate::mach::bind_opcodes::*;
        let location = if is_lazy {
            &self.lazy_location
        } else {
            &self.location
        };
        let mut bind_info = BindInformation::new(is_lazy);
        let mut offset = location.start;
        let mut start_of_sequence: usize = 0;
        while offset < location.end {
            let opcode = self.data.gread::<i8>(&mut offset)? as bind_opcodes::Opcode;
            // let mut input = String::new();
            // ::std::io::stdin().read_line(&mut input).unwrap();
            // println!("opcode: {} ({:#x}) offset: {:#x}\n {:?}", opcode_to_str(opcode & BIND_OPCODE_MASK), opcode, offset - location.start - 1, &bind_info);
            match opcode & BIND_OPCODE_MASK {
                // we do nothing, don't update our records, and add a new, fresh record
                BIND_OPCODE_DONE => {
                    bind_info = BindInformation::new(is_lazy);
                    start_of_sequence = offset - location.start;
                }
                BIND_OPCODE_SET_DYLIB_ORDINAL_IMM => {
                    let symbol_library_ordinal = opcode & BIND_IMMEDIATE_MASK;
                    bind_info.symbol_library_ordinal = symbol_library_ordinal;
                }
                BIND_OPCODE_SET_DYLIB_ORDINAL_ULEB => {
                    let symbol_library_ordinal = Uleb128::read(&self.data, &mut offset)?;
                    bind_info.symbol_library_ordinal = symbol_library_ordinal as u8;
                }
                BIND_OPCODE_SET_DYLIB_SPECIAL_IMM => {
                    // dyld puts the immediate into the symbol_library_ordinal field...
                    let special_dylib = opcode & BIND_IMMEDIATE_MASK;
                    // Printf.printf "special_dylib: 0x%x\n" special_dylib
                    bind_info.special_dylib = special_dylib;
                }
                BIND_OPCODE_SET_SYMBOL_TRAILING_FLAGS_IMM => {
                    let symbol_flags = opcode & BIND_IMMEDIATE_MASK;
                    let symbol_name = self.data.pread::<&str>(offset)?;
                    offset += symbol_name.len() + 1; // second time this \0 caused debug woes
                    bind_info.symbol_name = symbol_name;
                    bind_info.symbol_flags = symbol_flags;
                }
                BIND_OPCODE_SET_TYPE_IMM => {
                    let bind_type = opcode & BIND_IMMEDIATE_MASK;
                    bind_info.bind_type = bind_type;
                }
                BIND_OPCODE_SET_ADDEND_SLEB => {
                    let addend = Sleb128::read(&self.data, &mut offset)?;
                    bind_info.addend = addend;
                }
                BIND_OPCODE_SET_SEGMENT_AND_OFFSET_ULEB => {
                    let seg_index = opcode & BIND_IMMEDIATE_MASK;
                    // dyld sets the address to the segActualLoadAddress(segIndex) + uleb128
                    // address = segActualLoadAddress(segmentIndex) + read_uleb128(p, end);
                    let seg_offset = Uleb128::read(&self.data, &mut offset)?;
                    bind_info.seg_index = seg_index;
                    bind_info.seg_offset = seg_offset;
                }
                BIND_OPCODE_ADD_ADDR_ULEB => {
                    let addr = Uleb128::read(&self.data, &mut offset)?;
                    let seg_offset = bind_info.seg_offset.wrapping_add(addr);
                    bind_info.seg_offset = seg_offset;
                }
                // record the record by placing its value into our list
                BIND_OPCODE_DO_BIND => {
                    // from dyld:
                    //      if ( address >= segmentEndAddress )
                    // throwBadBindingAddress(address, segmentEndAddress, segmentIndex, start, end, p);
                    // (this->*handler)(context, address, type, symbolName, symboFlags, addend, libraryOrdinal, "", &last);
                    // address += sizeof(intptr_t);
                    imports.push(Import::new(&bind_info, libs, segments, start_of_sequence));
                    let seg_offset = bind_info.seg_offset.wrapping_add(ctx.size() as u64);
                    bind_info.seg_offset = seg_offset;
                }
                BIND_OPCODE_DO_BIND_ADD_ADDR_ULEB => {
                    // dyld:
                    // if ( address >= segmentEndAddress )
                    // throwBadBindingAddress(address, segmentEndAddress, segmentIndex, start, end, p);
                    // (this->*handler)(context, address, type, symbolName, symboFlags, addend, libraryOrdinal, "", &last);
                    // address += read_uleb128(p, end) + sizeof(intptr_t);
                    // we bind the old record, then increment bind info address for the next guy, plus the ptr offset *)
                    imports.push(Import::new(&bind_info, libs, segments, start_of_sequence));
                    let addr = Uleb128::read(&self.data, &mut offset)?;
                    let seg_offset = bind_info
                        .seg_offset
                        .wrapping_add(addr)
                        .wrapping_add(ctx.size() as u64);
                    bind_info.seg_offset = seg_offset;
                }
                BIND_OPCODE_DO_BIND_ADD_ADDR_IMM_SCALED => {
                    // dyld:
                    // if ( address >= segmentEndAddress )
                    // throwBadBindingAddress(address, segmentEndAddress, segmentIndex, start, end, p);
                    // (this->*handler)(context, address, type, symbolName, symboFlags, addend, libraryOrdinal, "", &last);
                    // address += immediate*sizeof(intptr_t) + sizeof(intptr_t);
                    // break;
                    // similarly, we bind the old record, then perform address manipulation for the next record
                    imports.push(Import::new(&bind_info, libs, segments, start_of_sequence));
                    let scale = opcode & BIND_IMMEDIATE_MASK;
                    let size = ctx.size() as u64;
                    let seg_offset = bind_info
                        .seg_offset
                        .wrapping_add(u64::from(scale) * size)
                        .wrapping_add(size);
                    bind_info.seg_offset = seg_offset;
                }
                BIND_OPCODE_DO_BIND_ULEB_TIMES_SKIPPING_ULEB => {
                    // dyld:
                    // count = read_uleb128(p, end);
                    // skip = read_uleb128(p, end);
                    // for (uint32_t i=0; i < count; ++i) {
                    // if ( address >= segmentEndAddress )
                    // throwBadBindingAddress(address, segmentEndAddress, segmentIndex, start, end, p);
                    // (this->*handler)(context, address, type, symbolName, symboFlags, addend, libraryOrdinal, "", &last);
                    // address += skip + sizeof(intptr_t);
                    // }
                    // break;
                    let count = Uleb128::read(&self.data, &mut offset)?;
                    let skip = Uleb128::read(&self.data, &mut offset)?;
                    let skip_plus_size = skip + ctx.size() as u64;
                    for _i in 0..count {
                        imports.push(Import::new(&bind_info, libs, segments, start_of_sequence));
                        let seg_offset = bind_info.seg_offset.wrapping_add(skip_plus_size);
                        bind_info.seg_offset = seg_offset;
                    }
                }
                _ => {}
            }
        }
        Ok(())
    }
}
