// Based on https://github.com/daniel5151/gdbstub/blob/master/examples/armv4t/emu.rs

use num_bigint::ToBigInt;
use std::collections::HashMap;
use std::rc::Rc;

use clvmr::error::EvalErr;
use clvmr::{Allocator, ChiaDialect, NodePtr, SExp, run_program};

use armv4t_emu::Cpu;
use armv4t_emu::Memory;
use armv4t_emu::Mode;
use armv4t_emu::reg;
use gdbstub::arch::Arch;
use gdbstub::common::Pid;
use gdbstub::target::ext::base::singlethread::{
    SingleThreadBase, SingleThreadResume, SingleThreadResumeOps,
};
use gdbstub::target::ext::base::{BaseOps, single_register_access};
use gdbstub::target::ext::breakpoints::{
    Breakpoints, BreakpointsOps, HwBreakpointOps, HwWatchpointOps, SwBreakpoint, SwBreakpointOps,
};
use gdbstub::target::{Target, TargetResult};

use clvm_to_arm_generate::clvmr_node::{get_number, proper_list, sha256tree};
use clvm_to_arm_generate::disassemble::disassemble;
use clvm_to_arm_generate::sexp::{Number, bi_one, bi_zero, u8_from_number};

use clvm_to_arm_generate::arm::{Encodable, Instr, Register};
use clvm_to_arm_generate::code::{
    NEXT_ALLOC_OFFSET, SWI_DISPATCH_INSTRUCTION, SWI_DISPATCH_NEW_CODE, SWI_DONE, SWI_PRINT_EXPR,
    SWI_THROW, TARGET_ADDR,
};
use clvm_to_arm_generate::loader::{ElfLoader, EmuSymbolInfo};
use clvm_to_arm_generate::mem::{PagedMemory, TargetMemory};

pub type DynResult<T> = Result<T, Box<dyn std::error::Error>>;

const HLE_RETURN_ADDR: u32 = 0x12345678;
const MAX_COST: u64 = 1000000000;

#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Event {
    DoneStep,
    Halted,
    Output,
    Trap,
    Break,
    WatchWrite(u32),
    WatchRead(u32),
}

pub enum ExecMode {
    Step,
    Continue,
    RangeStep(u32, u32),
}

/// Armv4t emulator wrapper based on https://github.com/daniel5151/armv4t_emu .
/// This operates specially on executables generated by clvm_to_arm_generate to provide
/// a way of running clvm code in gdb and making something useful and inspectable.
///
/// It doesn't emulate any real machine but instead provides a way of speaking with gdb
/// about clvm code by providing a simple system it can understand.
///
/// This can be run stand alone (run to completion) using run_to_exit, or an emulator
/// object can be created and used with the gdb stub in various ways (see clvm_to_arm_serve).
pub struct Emu {
    start_addr: u32,

    // example custom register. only read/written to from the GDB client
    pub custom_reg: u32,

    pub exec_mode: ExecMode,

    pub cpu: Cpu,
    pub mem: PagedMemory,

    pub watchpoints: Vec<u32>,
    pub breakpoints: Vec<u32>,

    pub env_addr: u32,
    pub reported_pid: Pid,

    pub clvm_symbols: Rc<HashMap<String, String>>,
    pub jit_symbols: Rc<HashMap<String, EmuSymbolInfo>>,

    pending_gdb_console_output: Vec<String>,
}

impl SingleThreadBase for Emu {
    /// Read the target's registers.
    fn read_registers(
        &mut self,
        regs: &mut <Self::Arch as Arch>::Registers,
    ) -> TargetResult<(), Self> {
        for i in 0..13 {
            regs.r[i] = self.cpu.reg_get(Mode::User, i as u8);
        }
        regs.sp = self.cpu.reg_get(Mode::User, reg::SP);
        regs.lr = self.cpu.reg_get(Mode::User, reg::LR);
        regs.pc = self.cpu.reg_get(Mode::User, reg::PC);
        Ok(())
    }

    /// Write the target's registers.
    fn write_registers(
        &mut self,
        _regs: &<Self::Arch as Arch>::Registers,
    ) -> TargetResult<(), Self> {
        todo!();
    }

    /// Support for single-register access.
    /// See [`SingleRegisterAccess`] for more details.
    ///
    /// While this is an optional feature, it is **highly recommended** to
    /// implement it when possible, as it can significantly improve performance
    /// on certain architectures.
    ///
    /// [`SingleRegisterAccess`]:
    /// super::single_register_access::SingleRegisterAccess
    #[inline(always)]
    fn support_single_register_access(
        &mut self,
    ) -> Option<single_register_access::SingleRegisterAccessOps<'_, (), Self>> {
        None
    }

    /// Read bytes from the specified address range and return the number of
    /// bytes that were read.
    ///
    /// Implementations may return a number `n` that is less than `data.len()`
    /// to indicate that memory starting at `start_addr + n` cannot be
    /// accessed.
    ///
    /// Implemenations may also return an appropriate non-fatal error if the
    /// requested address range could not be accessed (e.g: due to MMU
    /// protection, unhanded page fault, etc...).
    ///
    /// Implementations must guarantee that the returned number is less than or
    /// equal `data.len()`.
    fn read_addrs(
        &mut self,
        start_addr: <Self::Arch as Arch>::Usize,
        data: &mut [u8],
    ) -> TargetResult<usize, Self> {
        for (i, d) in data.iter_mut().enumerate() {
            *d = self.mem.r8(start_addr + i as u32);
        }
        Ok(data.len())
    }

    /// Write bytes to the specified address range.
    ///
    /// If the requested address range could not be accessed (e.g: due to
    /// MMU protection, unhanded page fault, etc...), an appropriate
    /// non-fatal error should be returned.
    fn write_addrs(
        &mut self,
        _start_addr: <Self::Arch as Arch>::Usize,
        _data: &[u8],
    ) -> TargetResult<(), Self> {
        todo!();
    }

    /// Support for resuming the target (e.g: via `continue` or `step`)
    #[inline(always)]
    fn support_resume(&mut self) -> Option<SingleThreadResumeOps<'_, Self>> {
        Some(self)
    }
}

impl Target for Emu {
    type Error = ();
    type Arch = gdbstub_arch::arm::Armv4t; // as an example

    #[inline(always)]
    fn base_ops(&mut self) -> BaseOps<'_, Self::Arch, Self::Error> {
        BaseOps::SingleThread(self)
    }

    // opt-in to support for setting/removing breakpoints
    #[inline(always)]
    fn support_breakpoints(&mut self) -> Option<BreakpointsOps<'_, Self>> {
        Some(self)
    }
}

impl SwBreakpoint for Emu {
    ///
    /// Return `Ok(false)` if the operation could not be completed.
    fn add_sw_breakpoint(
        &mut self,
        addr: <Self::Arch as Arch>::Usize,
        kind: <Self::Arch as Arch>::BreakpointKind,
    ) -> TargetResult<bool, Self> {
        self.breakpoints.push(addr);
        eprintln!("add breakpoint {kind:?} {addr}");
        Ok(true)
    }

    /// Remove an existing software breakpoint.
    ///
    /// Return `Ok(false)` if the operation could not be completed.
    fn remove_sw_breakpoint(
        &mut self,
        addr: <Self::Arch as Arch>::Usize,
        _kind: <Self::Arch as Arch>::BreakpointKind,
    ) -> TargetResult<bool, Self> {
        let found = self.breakpoints.iter().position(|u| *u == (addr));
        eprintln!("have breakpoint (to delete) {found:?}");
        if let Some(found) = found {
            self.breakpoints.remove(found);
            Ok(true)
        } else {
            Ok(false)
        }
    }
}

impl Breakpoints for Emu {
    /// Support for setting / removing software breakpoints.
    #[inline(always)]
    fn support_sw_breakpoint(&mut self) -> Option<SwBreakpointOps<'_, Self>> {
        Some(self)
    }

    /// Support for setting / removing hardware breakpoints.
    #[inline(always)]
    fn support_hw_breakpoint(&mut self) -> Option<HwBreakpointOps<'_, Self>> {
        None
    }

    /// Support for setting / removing hardware watchpoints.
    #[inline(always)]
    fn support_hw_watchpoint(&mut self) -> Option<HwWatchpointOps<'_, Self>> {
        None
    }
}

impl SingleThreadResume for Emu {
    fn resume(
        &mut self,
        _sig: std::option::Option<gdbstub::common::Signal>,
    ) -> Result<(), <Self as gdbstub::target::Target>::Error> {
        Ok(())
    }
}

pub fn atom_from_number(allocator: &mut Allocator, n: &Number) -> Result<NodePtr, EvalErr> {
    if *n == bi_zero() {
        return Ok(allocator.nil());
    }

    let bytes = u8_from_number(n);
    allocator.new_atom(&bytes)
}

pub fn generate_argument_refs(
    allocator: &mut Allocator,
    start: Number,
    sexp: NodePtr,
) -> Result<NodePtr, EvalErr> {
    match allocator.sexp(sexp) {
        SExp::Pair(_a, b) => {
            let next_index = bi_one() + 2_i32.to_bigint().unwrap() * start.clone();
            let tail = generate_argument_refs(allocator, next_index, b)?;
            let new_number = atom_from_number(allocator, &start)?;
            allocator.new_pair(new_number, tail)
        }
        _ => Ok(sexp),
    }
}

pub fn apply_op(
    allocator: &mut Allocator,
    head: NodePtr,
    args: NodePtr,
) -> Result<NodePtr, EvalErr> {
    let wrapped_args = allocator.new_pair(allocator.nil(), args)?;
    let generated_refs = generate_argument_refs(allocator, 5_i32.to_bigint().unwrap(), args)?;
    let application = allocator.new_pair(head, generated_refs)?;
    Ok(run_program(
        allocator,
        &ChiaDialect::new(0),
        application,
        wrapped_args,
        MAX_COST,
    )?
    .1)
}

fn is_print_atom(allocator: &Allocator, atom: NodePtr) -> bool {
    match allocator.sexp(atom) {
        SExp::Atom => allocator.atom(atom) == clvmr::Atom::Borrowed(b"*print*"),
        _ => false,
    }
}

fn is_print_request(allocator: &Allocator, args: NodePtr) -> Option<NodePtr> {
    if let SExp::Pair(f, r) = allocator.sexp(args)
        && is_print_atom(allocator, f)
    {
        return Some(r);
    }

    None
}

fn is_apply(allocator: &Allocator, sexp: NodePtr) -> bool {
    if let SExp::Atom = allocator.sexp(sexp) {
        return allocator.atom(sexp).to_vec() == [2];
    }
    false
}

fn is_apply_operator(allocator: &Allocator, sexp: NodePtr) -> bool {
    if let SExp::Pair(h, _t) = allocator.sexp(sexp) {
        return is_apply(allocator, h);
    }

    false
}

fn is_quote(allocator: &Allocator, sexp: NodePtr) -> bool {
    if let SExp::Atom = allocator.sexp(sexp) {
        return allocator.atom(sexp).to_vec() == [1];
    }
    false
}

fn is_quote_operator(allocator: &Allocator, sexp: NodePtr) -> bool {
    if let SExp::Pair(h, _t) = allocator.sexp(sexp) {
        return is_quote(allocator, h);
    }

    false
}

fn match_printing(
    allocator: &Allocator,
    operator: NodePtr,
    sexp: NodePtr,
) -> Option<(bool, NodePtr)> {
    if let Some(v) = get_number(allocator, operator)
        && v == 34_u32.to_bigint().unwrap()
    {
        return is_print_request(allocator, sexp).map(|p| (false, p));
    }

    if let SExp::Atom = allocator.sexp(operator)
        && allocator.atom(operator) == clvmr::Atom::Borrowed(b"debug_print")
    {
        return Some((true, sexp));
    }

    None
}

impl Emu {
    fn get_nul_terminated_string(&self, addr: u32) -> Option<String> {
        if addr == 0 {
            return None;
        }

        let mut out = Vec::new();
        for i in 0..1024_u32 {
            let b = self.mem.read_u8(addr + i);
            if b == 0 {
                return Some(String::from_utf8_lossy(&out).to_string());
            }
            out.push(b);
        }

        None
    }

    fn lookup_dispatch_target_by_hash(&self, hash_hex: &str) -> Option<u32> {
        if let Some(lookup) = self.jit_symbols.get(hash_hex) {
            return Some(lookup.address);
        }

        let alias_symbol = format!("_$_{hash_hex}");
        if let Some(alias_info) = self.jit_symbols.get(&alias_symbol)
            && let Some(alias_name) = self.get_nul_terminated_string(alias_info.address)
            && let Some(lookup) = self.jit_symbols.get(&alias_name)
        {
            return Some(lookup.address);
        }

        None
    }

    /// Given a clvm allocator and an env NodePtr allocated in it, the program data and
    /// the symbol map from the program, create an Emu object which allows us to run
    /// clvm code translated to arm.
    ///
    /// The code will contain svc instructions which communicate with the emulator.
    /// It terminates abnormally when svc 1 is executed, and normally for svc 0.
    /// The emulator expects the mem object to provide any address space it asks for.
    pub fn new(
        allocator: &mut Allocator,
        program_elf: &[u8],
        env_node: NodePtr,
        clvm_symbols: Rc<HashMap<String, String>>,
    ) -> DynResult<Emu> {
        // set up emulated system
        let mut cpu = Cpu::new();
        let mut mem = PagedMemory::default();

        // copy all in-memory sections from the ELF file into system RAM
        let elf_loader = ElfLoader::new(program_elf, TARGET_ADDR).expect("should load");
        elf_loader.load(&mut mem);

        let jit_symbols = Rc::new(elf_loader.get_symbols());

        cpu.reg_set(Mode::User, reg::SP, 0xffffff00);
        cpu.reg_set(Mode::User, reg::LR, HLE_RETURN_ADDR);
        cpu.reg_set(Mode::User, reg::PC, TARGET_ADDR);
        cpu.reg_set(Mode::User, reg::CPSR, 0x10); // user mode

        let mut emu = Emu {
            start_addr: TARGET_ADDR,
            env_addr: 0,

            custom_reg: 0x12345678,

            exec_mode: ExecMode::Continue,

            cpu,
            mem,

            watchpoints: Vec::new(),
            breakpoints: Vec::new(),

            reported_pid: Pid::new(1).unwrap(),

            jit_symbols,
            clvm_symbols,

            pending_gdb_console_output: Vec::new(),
        };

        // Allocate the environment on the stack.
        let alloc_ptr = emu.cpu.reg_get(Mode::User, reg::SP);
        emu.mem.write_u32(alloc_ptr, alloc_ptr);
        emu.env_addr = emu.allocate_and_write(
            allocator,
            alloc_ptr,
            env_node,
            false
        );

        // Env in r1.
        emu.cpu.reg_set(Mode::User, 1, emu.env_addr);
        // Put the stack below the environment.
        let new_stack = emu.mem.read_u32(alloc_ptr);
        emu.cpu.reg_set(Mode::User, reg::SP, new_stack & !15);

        Ok(emu)
    }

    pub fn reset(&mut self) {
        self.cpu.reg_set(Mode::User, 1, self.env_addr);
        let new_stack = self.mem.read_u32(0xffffff00);
        self.cpu.reg_set(Mode::User, reg::SP, new_stack & !15);
        self.cpu.reg_set(Mode::User, reg::LR, HLE_RETURN_ADDR);
        self.cpu.reg_set(Mode::User, reg::PC, self.start_addr);
        self.cpu.reg_set(Mode::User, reg::CPSR, 0x10);
    }

    pub fn take_pending_gdb_console_output(&mut self) -> Vec<String> {
        std::mem::take(&mut self.pending_gdb_console_output)
    }

    fn allocate_and_write(
        &mut self,
        allocator: &mut Allocator,
        alloc_ptr: u32,
        sexp: NodePtr,
        grow_up: bool
    ) -> u32 {
        let current_addr = self.mem.read_u32(alloc_ptr);
        let grow =
            if grow_up {
                |x: u32, n: u32| { (x, x + n) }
            } else {
                |x: u32, n: u32| { (x - n, x - n) }
            };
        match allocator.sexp(sexp) {
            SExp::Pair(a, b) => {
                let (target_addr, new_addr) = grow(current_addr, 8);
                self.mem.write_u32(alloc_ptr, new_addr);
                let a_res = self.allocate_and_write(allocator, alloc_ptr, a, grow_up);
                let b_res = self.allocate_and_write(allocator, alloc_ptr, b, grow_up);
                self.mem.write_u32(target_addr, a_res);
                self.mem.write_u32(target_addr + 4, b_res);
                target_addr
            }
            SExp::Atom => {
                let v = allocator.atom(sexp);
                let length_to_write = ((v.len() + 3) & !3) as u32;
                let (target_addr, new_addr) = grow(current_addr, length_to_write + 4);
                self.mem
                    .write_u32(alloc_ptr, new_addr);
                self.mem.write_u32(target_addr, v.len() as u32 * 2 + 1);
                self.mem.write_data(&v, target_addr + 4);
                target_addr
            }
        }
    }

    fn do_apply_op(
        &mut self,
        allocator: &mut Allocator,
        operator: NodePtr,
        args: NodePtr,
    ) -> Option<Event> {
        let alloc_ptr = self.cpu.reg_get(Mode::User, 5);
        let mut debug = false;
        let mut assume_nil = false;

        if let Some((nil, printing)) = match_printing(allocator, operator, args) {
            self.pending_gdb_console_output
                .push(format!("DEBUG: {}", disassemble(allocator, printing)));
            debug = true;
            assume_nil = nil;
        }

        let result = if assume_nil {
            Ok(allocator.nil())
        } else {
            apply_op(allocator, operator, args)
        };

        match result {
            Ok(res) => {
                // Allocate and write back result.
                let write_result = self.allocate_and_write(allocator, alloc_ptr, res, true);
                self.cpu.reg_set(Mode::User, 0, write_result);
                // Increment pc, we handled the operation.
                let pc = self.cpu.reg_get(Mode::User, reg::PC);
                self.cpu.reg_set(Mode::User, reg::PC, pc + 4);
                if debug { Some(Event::Output) } else { None }
            }
            Err(e) => {
                eprintln!("error simulating instruction: {e:?}");
                Some(Event::Trap)
            }
        }
    }

    fn do_trap(&mut self, pc: u32, value: usize) -> Option<Event> {
        match self.do_trap_(pc, value) {
            Ok(res) => res,
            Err(e) => {
                self.pending_gdb_console_output
                    .push(format!("CLVM Error: {e:?}"));
                Some(Event::Trap)
            }
        }
    }

    fn do_trap_(&mut self, pc: u32, value: usize) -> Result<Option<Event>, EvalErr> {
        if value == SWI_DONE {
            Ok(Some(Event::Halted))
        } else if value == SWI_THROW {
            Ok(Some(Event::Trap))
        } else if value == SWI_DISPATCH_NEW_CODE {
            let mut allocator = Allocator::new();
            let r0_value = self.cpu.reg_get(Mode::User, 0);
            let to_run = self.get_sexp(&mut allocator, r0_value)?;

            let env_value = self.cpu.reg_get(Mode::User, 7);
            let env = self.get_sexp(&mut allocator, env_value)?;

            let hash = sha256tree(&allocator, to_run);
            let string_of_hash = hex::encode(&hash);

            // We have unknown code in to_run.
            //
            // There are two cases:
            //
            // 1) jit_symbols contains a match for the hash of to_run.
            //    In that case, we can transfer control to that function as though
            //    it was a function call.
            //
            // 2) jit_symbols does not contain a symbol for this.  In this case,
            //    we allocate space using the allocation ptr in r5 and generate
            //    code for the first operator in the given clvm with each argument
            //    computed via an SWI_DISPATCH_NEW_CODE instruction.
            //
            //    We will keep reentering the emulator this way until we find
            //    a match or emit a primitive instruction that is freestanding.

            // Setup stack frame in code buffer.

            let current_pc = self.cpu.reg_get(Mode::User, reg::PC);
            if let Some(dispatch_address) = self.lookup_dispatch_target_by_hash(&string_of_hash) {
                // We found it, transfer control.
                self.cpu
                    .reg_set(Mode::User, 0, self.cpu.reg_get(Mode::User, 7));
                self.cpu.reg_set(Mode::User, reg::LR, current_pc + 8);
                self.cpu.reg_set(Mode::User, reg::PC, current_pc + 4);
                self.cpu.reg_set(Mode::User, 1, dispatch_address);
                return Ok(None);
            };

            // Quoted is easy.
            if is_quote_operator(&allocator, to_run) {
                self.cpu.reg_set(Mode::User, 0, self.mem.r32(r0_value + 4));
                self.cpu.reg_set(Mode::User, 1, current_pc + 8);
                self.cpu.reg_set(Mode::User, reg::PC, current_pc + 4);
                return Ok(None);
            }

            if let Some(_path) = get_number(&allocator, to_run) {
                // Path retrieval.
                let new_env_tail = allocator.new_pair(env, allocator.nil())?;
                let new_expr = allocator.new_pair(to_run, new_env_tail)?;
                let new_apply_atom = allocator.new_atom(&[2])?;
                if let Some(error) = self.do_apply_op(&mut allocator, new_apply_atom, new_expr) {
                    return Ok(Some(error));
                }
                self.cpu.reg_set(Mode::User, 1, current_pc + 8);
                return Ok(None);
            }

            let mut address_list = vec![];
            let mut value_addr = r0_value;
            while value_addr != 0 && self.mem.r32(value_addr).is_multiple_of(2) {
                address_list.push(value_addr);
                value_addr = self.mem.r32(value_addr + 4);
            }
            let apply_operator = is_apply_operator(&allocator, to_run);
            let arg_addresses: Vec<u32> = address_list
                .iter()
                .skip(1)
                .map(|arg_cons_addr| self.mem.r32(*arg_cons_addr))
                .collect();

            if (value_addr & 1) != 0 && (value_addr >> 1) != 0 {
                // Not a proper list.
                return Ok(Some(Event::Trap));
            }

            let alloc_address = self.cpu.reg_get(Mode::User, 5);
            // Structure of data area:
            //                                                offset
            // pointer to next argument                       0
            // pointer to cons                                4
            // operator address                               8
            // code                                           12
            // reverse order argument addresses               after code

            let new_code_address = self.mem.r32(alloc_address);

            // Emit code for each argument in reverse order, accumulating into r0.
            let mut instruction_list = vec![
                // Values on the stack:
                // Pointer to first argument pointer.  Will be fixed up.
                Instr::Long(0),
                // Constructed value for operator evaluation.
                Instr::Long(0),
                // Operator sexp.
                Instr::Long(self.mem.r32(r0_value) as usize),
                // Push the stack for this.
                Instr::Push(vec![Register::FP, Register::LR]),
                Instr::Addi(Register::FP, Register::SP, 4),
                Instr::Subi(Register::SP, Register::SP, 0x18),
                Instr::Str(Register::R(4), Register::SP, 0),
                Instr::Str(Register::R(5), Register::SP, 4),
                Instr::Str(Register::R(6), Register::SP, 8),
                Instr::Str(Register::R(7), Register::SP, 12),
            ];

            instruction_list.push(Instr::Subi(
                Register::R(6),
                Register::PC,
                4 * (instruction_list.len() + 2) as i32,
            ));

            // Handle an apply instruction inline.
            if apply_operator {
                // It acts as throw when it doesn't have the right arguments.
                if !matches!(proper_list(&allocator, to_run).map(|l| l.len()), Some(3)) {
                    return Ok(Some(Event::Break));
                }

                // Load new env ptr.
                // It's the second head of this cons chain.
                instruction_list.push(Instr::Ldr(Register::R(0), Register::R(6), 0));
                instruction_list.push(Instr::Ldr(Register::R(0), Register::R(0), 4));
                // Evalute env.
                instruction_list.push(Instr::Swi(SWI_DISPATCH_NEW_CODE));
                instruction_list.push(Instr::Bx(Register::R(1)));
                // Save env ptr in r4.
                instruction_list.push(Instr::Addi(Register::R(4), Register::R(0), 0));
                // Load code.
                instruction_list.push(Instr::Ldr(Register::R(0), Register::R(6), 0));
                instruction_list.push(Instr::Ldr(Register::R(0), Register::R(0), 0));
                // Evaluate code.
                instruction_list.push(Instr::Swi(SWI_DISPATCH_NEW_CODE));
                instruction_list.push(Instr::Bx(Register::R(1)));
                // Load env from r4.
                instruction_list.push(Instr::Addi(Register::R(7), Register::R(4), 0));

                // Dispatch the actual code.
                instruction_list.push(Instr::Swi(SWI_DISPATCH_NEW_CODE));
                instruction_list.push(Instr::Bx(Register::R(1)));
                instruction_list.push(Instr::Ldr(Register::R(7), Register::SP, 12));
            } else {
                // Arguments in env are in a proper list.  Emit code to iterate it from end to start,
                for i in (0..arg_addresses.len()).rev() {
                    instruction_list.push(Instr::Ldr(Register::R(1), Register::R(6), 0));
                    instruction_list.push(Instr::Ldr(
                        Register::R(0),
                        Register::R(1),
                        (i * 4) as i32,
                    ));
                    // Now we have the code to dispatch in r0.
                    instruction_list.push(Instr::Swi(SWI_DISPATCH_NEW_CODE));
                    // Follow dispatcher-selected code target.
                    instruction_list.push(Instr::Bx(Register::R(1)));
                    // Result is in R0.
                    // Allocate a cons and compose it.
                    instruction_list.push(Instr::Ldr(
                        Register::R(2),
                        Register::R(5),
                        NEXT_ALLOC_OFFSET,
                    ));
                    // Bump r2 to point to the next unallocated space.
                    instruction_list.push(Instr::Addi(Register::R(3), Register::R(2), 8));
                    // Update the allocation ptr.
                    instruction_list.push(Instr::Str(
                        Register::R(3),
                        Register::R(5),
                        NEXT_ALLOC_OFFSET,
                    ));
                    // Set the head of the cons to the newly evaluated argument.
                    instruction_list.push(Instr::Str(Register::R(0), Register::R(2), 0));
                    // Tail points at the currently built argument list.
                    instruction_list.push(Instr::Ldr(Register::R(3), Register::R(6), 4));
                    instruction_list.push(Instr::Str(Register::R(3), Register::R(2), 4));
                    // Set the new cons ptr.
                    instruction_list.push(Instr::Str(Register::R(2), Register::R(6), 4));
                }

                // Load the operator address into R0
                instruction_list.push(Instr::Ldr(Register::R(0), Register::R(6), 8));
                // Load the args address into R1
                instruction_list.push(Instr::Ldr(Register::R(1), Register::R(6), 4));
                // Emit dispatch instruction.
                instruction_list.push(Instr::Swi(SWI_DISPATCH_INSTRUCTION));
            }

            instruction_list.push(Instr::Ldr(Register::R(4), Register::SP, 0));
            instruction_list.push(Instr::Ldr(Register::R(5), Register::SP, 4));
            instruction_list.push(Instr::Ldr(Register::R(6), Register::SP, 8));
            instruction_list.push(Instr::Ldr(Register::R(7), Register::SP, 12));
            instruction_list.push(Instr::Subi(Register::SP, Register::FP, 4));
            instruction_list.push(Instr::Pop(vec![Register::FP, Register::LR]));
            instruction_list.push(Instr::Bx(Register::LR));

            instruction_list[0] =
                Instr::Long(new_code_address as usize + 4 * instruction_list.len());
            for arg in arg_addresses.iter() {
                instruction_list.push(Instr::Long(*arg as usize));
            }

            // Allocate space for this thunk.
            self.mem.write_u32(
                alloc_address,
                new_code_address + instruction_list.len() as u32 * 4,
            );

            let mut relocations = Vec::new();
            for (i, instr) in instruction_list.iter().enumerate() {
                let mut encoded = Vec::new();
                instr.encode(&mut encoded, &mut relocations, "");
                self.mem.write_u32(
                    new_code_address + (i * 4) as u32,
                    encoded[0] as u32
                        | (encoded[1] as u32) << 8
                        | (encoded[2] as u32) << 16
                        | (encoded[3] as u32) << 24,
                );
            }

            self.cpu.reg_set(Mode::User, reg::LR, current_pc + 8);
            self.cpu.reg_set(Mode::User, reg::PC, current_pc + 4);
            self.cpu.reg_set(Mode::User, 1, new_code_address + 12);
            Ok(None)
        } else if value == SWI_DISPATCH_INSTRUCTION {
            let mut allocator = Allocator::new();
            // Grab the sexp for this operation.
            let r0_value = self.cpu.reg_get(Mode::User, 0);
            let operator = self.get_sexp(&mut allocator, r0_value)?;
            let r1_value = self.cpu.reg_get(Mode::User, 1);
            let args = self.get_sexp(&mut allocator, r1_value)?;
            Ok(self.do_apply_op(&mut allocator, operator, args))
        } else if (value & 15) == SWI_PRINT_EXPR {
            let mut allocator = Allocator::new();
            let register = (value >> 4) & 15;
            let label = value >> 8;
            let r0_value = self.cpu.reg_get(Mode::User, register as u8);
            let print_arg = self.get_sexp(&mut allocator, r0_value)?;
            let printed_expr = disassemble(&allocator, print_arg);
            if label != 0 || register != 0 {
                self.pending_gdb_console_output
                    .push(format!("CLVM({label:x}): r{register} = {printed_expr}"));
            } else {
                self.pending_gdb_console_output
                    .push(format!("CLVM: {printed_expr}"));
            }
            self.cpu.reg_set(Mode::User, reg::PC, pc + 4);
            Ok(Some(Event::Output))
        } else {
            self.cpu.reg_set(Mode::User, reg::PC, pc + 4);
            Ok(Some(Event::Break))
        }
    }

    /// single-step the interpreter
    pub fn step(&mut self) -> Option<Event> {
        // let mut hit_watchpoint = None;

        let pc = self.cpu.reg_get(Mode::User, reg::PC);
        let snoop_instruction = self.mem.r32(pc);

        if (snoop_instruction & 0x0f000000) == 0x0f000000 {
            // This is a trap instruction, interpret it.
            let cpsr = self.cpu.reg_get(Mode::User, reg::CPSR);
            let match_expression = snoop_instruction >> 28;
            let perform_action = match match_expression {
                0 => ((cpsr >> 30) & 1) != 0,
                10 => ((cpsr >> 31) & 1) == ((cpsr >> 28) & 1),
                14 => true,
                _ => todo!("match arm condition {match_expression}"),
            };
            if perform_action {
                let trap_result = self.do_trap(pc, (snoop_instruction & 0xffffff) as usize);
                if trap_result.is_some() {
                    return trap_result;
                }
            } else {
                self.cpu.reg_set(Mode::User, reg::PC, pc + 4);
            }
        } else {
            self.cpu.step(&mut self.mem);
        }

        let pc = self.cpu.reg_get(Mode::User, reg::PC);

        if self.breakpoints.contains(&pc) {
            return Some(Event::Break);
        }

        // if let Some(access) = hit_watchpoint {
        //     let fixup = if self.cpu.thumb_mode() { 2 } else { 4 };
        //     self.cpu.reg_set(Mode::User, reg::PC, pc - fixup);

        //     return Some(match access.kind {
        //         AccessKind::Read => Event::WatchRead(access.addr),
        //         AccessKind::Write => Event::WatchWrite(access.addr),
        //     });
        // }

        None
    }

    /// run the emulator in accordance with the currently set `ExecutionMode`.
    ///
    /// since the emulator runs in the same thread as the GDB loop, the emulator
    /// will use the provided callback to poll the connection for incoming data
    /// every 1024 steps.
    pub fn run(&mut self, mut poll_incoming_data: impl FnMut() -> bool) -> RunEvent {
        match self.exec_mode {
            ExecMode::Step => RunEvent::Event(self.step().unwrap_or(Event::DoneStep)),
            ExecMode::Continue => {
                let mut cycles = 0;
                loop {
                    if cycles % 1024 == 0 {
                        // poll for incoming data
                        if poll_incoming_data() {
                            break RunEvent::IncomingData;
                        }
                    }
                    cycles += 1;

                    if let Some(event) = self.step() {
                        break RunEvent::Event(event);
                    };
                }
            }
            // just continue, but with an extra PC check
            ExecMode::RangeStep(start, end) => {
                let mut cycles = 0;
                loop {
                    if cycles % 1024 == 0 {
                        // poll for incoming data
                        if poll_incoming_data() {
                            break RunEvent::IncomingData;
                        }
                    }
                    cycles += 1;

                    if let Some(event) = self.step() {
                        break RunEvent::Event(event);
                    };

                    if !(start..end).contains(&self.cpu.reg_get(self.cpu.mode(), reg::PC)) {
                        break RunEvent::Event(Event::DoneStep);
                    }
                }
            }
        }
    }
}

impl Emu {
    /// Get an SExp at a specific address.
    pub fn get_sexp(&self, allocator: &mut Allocator, addr: u32) -> Result<NodePtr, EvalErr> {
        if addr == 0 {
            return Ok(allocator.nil());
        }
        let first = self.mem.read_u32(addr);
        if first == 0 || (first & 1) != 0 {
            // Atom
            let size = first >> 1;
            let result: Vec<u8> = (0..size).map(|i| self.mem.read_u8(addr + 4 + i)).collect();
            allocator.new_atom(&result)
        } else {
            // Cons
            let rest = self.mem.read_u32(addr + 4);
            let f = self.get_sexp(allocator, first)?;
            let r = self.get_sexp(allocator, rest)?;
            allocator.new_pair(f, r)
        }
    }

    /// Run to completion and return a value by address for tests.
    /// This is a convenient way to run a whole program and get either None (throw) or
    /// Some(value) out, allowing one to determine whether the underlying code performed
    /// properly.
    pub fn run_to_exit(
        allocator: &mut Allocator,
        program: &[u8],
        env: NodePtr,
        clvm_symbols: Rc<HashMap<String, String>>,
    ) -> DynResult<Option<NodePtr>> {
        let mut emu = Emu::new(allocator, program, env, clvm_symbols)?;
        let elf_loader = ElfLoader::new(program, TARGET_ADDR).expect("should load");
        elf_loader.load(&mut emu.mem);

        loop {
            let step_result = emu.step();
            match step_result {
                Some(Event::Halted) => {
                    let r0 = emu.cpu.reg_get(Mode::User, 0);
                    return Ok(Some(emu.get_sexp(allocator, r0).expect("should read")));
                }
                Some(Event::Trap) => {
                    return Ok(None);
                }
                _ => {}
            }
        }
    }
}

pub enum RunEvent {
    IncomingData,
    Event(Event),
}
