from typing import Optional

class ArchTools:
    """ Exploitation helper for chip architectures """
    def __init__(self, data: bytes, base_addr: int):
        self.data = data
        self.base_addr = base_addr

    def va_to_offset(self, va: int) -> Optional[int]:
        """
        Convert a virtual address offset to a file offset.
        """
        off = va - self.base_addr
        if off < 0 or off >= len(self.data):
            return None
        return off

    def offset_to_va(self, offset: int) -> Optional[int]:
        """
        Convert a file offset to a virtual address.
        """
        if offset < 0 or offset >= len(self.data):
            return None
        return self.base_addr + offset

    def is_bx_lr(self, instr: int) -> bool:
        _ = instr
        assert False, "Not implemented"

    def resolve_register_value(self, at: int, reg: int, lookahead: int) -> Optional[int]:
        _ = at, reg, lookahead
        assert False, "Not implemented"

    def resolve_register_value_back(self, at: int, reg: int, lookback: int) -> Optional[int]:
        _ = at, reg, lookback
        assert False, "Not implemented"

    def read_u32(self, offset: int) -> Optional[int]:
        if offset + 4 > len(self.data):
            return None
        return int.from_bytes(self.data[offset:offset + 4], 'little')

    def read_u64(self, offset: int) -> Optional[int]:
        if offset + 8 > len(self.data):
            return None
        return int.from_bytes(self.data[offset:offset + 8], 'little')

    def find_string(self, s: str) -> Optional[int]:
        """
        Find the starting offset of the string 's' in the binary data (null-terminated or not).
        Searches for both null-terminated and non-terminated versions.
        Exact port of Rust find_string logic.
        """
        target_bytes = s.encode('utf-8')
        # First search for null-terminated version
        with_null = target_bytes + b'\x00'
        pos = self.data.find(with_null)
        if pos != -1:
            return pos
        # Fallback to non-null-terminated
        pos = self.data.find(target_bytes)
        if pos != -1:
            return pos
        return None


    def get_data(self) -> bytes:
        return self.data

    def decode_bl(self, instr: int, pc: int) -> Optional[int]:
        _ = instr, pc
        assert False, "Not implemented"

    def get_bl_target(self, offset: int) -> Optional[int]:
        """ Get target addr for bl call """
        instr = self.read_u32(offset)
        if instr is None:
            return None
        pc = self.offset_to_va(offset)
        if pc is None:
            return None
        return self.decode_bl(instr, pc)

    def get_next_bl_from_off(self, offset: int) -> Optional[int]:
        off = offset
        while off < len(self.data):
            if self.get_bl_target(off) is not None:
                return off
            off += 4
        return None

    def get_next_b_from_off(self, offset: int) -> Optional[int]:
        return self.get_next_bl_from_off(offset)

    def get_b_target(self, offset: int) -> Optional[int]:
        return self.get_bl_target(offset)

    def get_previous_bl_from_off(self, offset: int) -> Optional[int]:
        """
        Search backwards from 'offset' to find the previous BL instruction offset.
        Scans up to a reasonable limit (e.g., 0x1000 bytes) to avoid infinite loops.
        Returns the offset of the BL or None if not found.
        """
        scan_off = offset - 4  # Start before the given offset
        limit = max(0, offset - 0x1000)  # Arbitrary backward limit (4KB / 1024 instructions)
        while scan_off >= limit:
            if self.get_bl_target(scan_off) is not None:
                return scan_off
            scan_off -= 4
        return None
