// Single-step execution, breakpoints and map-aware helper dispatch.

pub(all) enum StepResult {
  Continue
  Halted(UInt64)
  BreakpointHit(Int)
}

pub(all) struct Snapshot {
  pc : Int
  steps : Int
  halted : Bool
  retval : UInt64
  regs : Array[UInt64]
}

pub(all) struct ConditionalBreak {
  pc : Int
  reg : Int
  value : UInt64
}

pub(all) struct Machine {
  program : Array[Insn]
  vm : Vm
  mut pc : Int
  mut steps : Int
  max_steps : Int
  mut halted : Bool
  mut retval : UInt64
  breakpoints : Array[Int]
  conditions : Array[ConditionalBreak]
  call_stack : Array[Int]
  call_saved : Array[UInt64]
  executed : Array[Int]
  max_call_depth : Int
}

pub fn Machine::new(
  program : Array[Insn],
  input : Array[Int],
  max_steps : Int,
) -> Machine {
  {
    program,
    vm: Vm::new(input, max_steps),
    pc: 0,
    steps: 0,
    max_steps,
    halted: false,
    retval: 0UL,
    breakpoints: Array::new(),
    conditions: Array::new(),
    call_stack: Array::new(),
    call_saved: Array::new(),
    executed: Array::new(),
    max_call_depth: 8,
  }
}

pub fn Machine::new_default(
  program : Array[Insn],
  input : Array[Int],
) -> Machine {
  Machine::new(program, input, 10000)
}

pub fn Machine::add_array_map(
  self : Machine,
  key_size : Int,
  value_size : Int,
  max_entries : Int,
) -> Int raise BpfError {
  self.vm.add_array_map(key_size, value_size, max_entries)
}

pub fn Machine::set_breakpoint(self : Machine, pc : Int) -> Unit {
  for existing in self.breakpoints {
    if existing == pc {
      return
    }
  }
  self.breakpoints.push(pc)
}

pub fn Machine::clear_breakpoints(self : Machine) -> Unit {
  while self.breakpoints.length() > 0 {
    ignore(self.breakpoints.pop())
  }
  while self.conditions.length() > 0 {
    ignore(self.conditions.pop())
  }
}

pub fn Machine::set_conditional_breakpoint(
  self : Machine,
  pc : Int,
  reg : Int,
  value : UInt64,
) -> Unit raise BpfError {
  check_reg(reg)
  self.conditions.push({ pc, reg, value })
}

fn is_breakpoint(self : Machine, pc : Int) -> Bool {
  for existing in self.breakpoints {
    if existing == pc {
      return true
    }
  }
  for condition in self.conditions {
    if condition.pc == pc && self.vm.regs[condition.reg].equal(condition.value) {
      return true
    }
  }
  false
}

pub fn Machine::snapshot(self : Machine) -> Snapshot {
  let regs : Array[UInt64] = Array::new()
  for value in self.vm.regs {
    regs.push(value)
  }
  {
    pc: self.pc,
    steps: self.steps,
    halted: self.halted,
    retval: self.retval,
    regs,
  }
}

pub fn Machine::coverage(self : Machine) -> Array[Int] {
  let out : Array[Int] = Array::new()
  for value in self.executed {
    out.push(value)
  }
  out
}

pub fn Machine::execution_count(self : Machine, pc : Int) -> Int {
  if pc < 0 || pc >= self.executed.length() {
    0
  } else {
    self.executed[pc]
  }
}

fn step_once(
  program : Array[Insn],
  vm : Vm,
  pc : Int,
  call_stack : Array[Int],
  call_saved : Array[UInt64],
  max_call_depth : Int,
) -> (Int, UInt64?) raise BpfError {
  let ins = program[pc]
  let op = ins.op
  let mut next_pc = pc + 1
  if op == 0x07 || op == 0x0f || op == 0x17 || op == 0x1f ||
    op == 0x27 || op == 0x2f || op == 0x37 || op == 0x3f ||
    op == 0x47 || op == 0x4f || op == 0x57 || op == 0x5f ||
    op == 0x67 || op == 0x6f || op == 0x77 || op == 0x7f ||
    op == 0x87 || op == 0x97 || op == 0x9f || op == 0xa7 ||
    op == 0xaf || op == 0xb7 || op == 0xbf || op == 0xc7 || op == 0xcf {
    check_dst(ins.dst)
    let src = if (op & 0x08) != 0 {
      check_reg(ins.src)
      vm.regs[ins.src]
    } else {
      imm_u64(ins.imm)
    }
    vm.regs[ins.dst] = alu64(
      op & 0xf7, vm.regs[ins.dst], src, ins.off == 1,
    )
  } else if op == 0x04 || op == 0x0c || op == 0x14 || op == 0x1c ||
    op == 0x24 || op == 0x2c || op == 0x34 || op == 0x3c ||
    op == 0x44 || op == 0x4c || op == 0x54 || op == 0x5c ||
    op == 0x64 || op == 0x6c || op == 0x74 || op == 0x7c ||
    op == 0x84 || op == 0x94 || op == 0x9c || op == 0xa4 ||
    op == 0xac || op == 0xb4 || op == 0xbc || op == 0xc4 || op == 0xcc {
    check_dst(ins.dst)
    let src = if (op & 0x08) != 0 {
      check_reg(ins.src)
      vm.regs[ins.src]
    } else {
      imm_u64(ins.imm)
    }
    vm.regs[ins.dst] = alu32(
      op & 0xf7, vm.regs[ins.dst], src, ins.off == 1,
    )
  } else if op == 0x61 || op == 0x69 || op == 0x71 || op == 0x79 {
    check_dst(ins.dst)
    check_reg(ins.src)
    let size = if op == 0x61 {
      4
    } else if op == 0x69 {
      2
    } else if op == 0x71 {
      1
    } else {
      8
    }
    let addr = vm.regs[ins.src].add(imm_u64(ins.off))
    vm.regs[ins.dst] = vm.load(addr, size)
  } else if op == 0x81 || op == 0x89 || op == 0x91 {
    check_dst(ins.dst)
    check_reg(ins.src)
    let size = if op == 0x81 {
      4
    } else if op == 0x89 {
      2
    } else {
      1
    }
    let addr = vm.regs[ins.src].add(imm_u64(ins.off))
    vm.regs[ins.dst] = sign_extend_n(vm.load(addr, size), size * 8)
  } else if op == 0x63 || op == 0x6b || op == 0x73 || op == 0x7b {
    check_reg(ins.dst)
    check_reg(ins.src)
    let size = if op == 0x63 {
      4
    } else if op == 0x6b {
      2
    } else if op == 0x73 {
      1
    } else {
      8
    }
    let addr = vm.regs[ins.dst].add(imm_u64(ins.off))
    vm.store(addr, size, vm.regs[ins.src])
  } else if op == 0x62 || op == 0x6a || op == 0x72 || op == 0x7a {
    check_reg(ins.dst)
    let size = if op == 0x62 {
      4
    } else if op == 0x6a {
      2
    } else if op == 0x72 {
      1
    } else {
      8
    }
    let addr = vm.regs[ins.dst].add(imm_u64(ins.off))
    vm.store(addr, size, imm_u64(ins.imm))
  } else if op == 0x05 {
    next_pc = pc + 1 + ins.off
  } else if op == 0x15 || op == 0x1d || op == 0x25 || op == 0x2d ||
    op == 0x35 || op == 0x3d || op == 0x45 || op == 0x4d ||
    op == 0x55 || op == 0x5d || op == 0x65 || op == 0x6d ||
    op == 0x75 || op == 0x7d || op == 0xa5 || op == 0xad ||
    op == 0xb5 || op == 0xbd || op == 0xc5 || op == 0xcd ||
    op == 0xd5 || op == 0xdd {
    check_reg(ins.dst)
    let src = if (op & 0x08) != 0 {
      check_reg(ins.src)
      vm.regs[ins.src]
    } else {
      imm_u64(ins.imm)
    }
    if jump_taken(op, vm.regs[ins.dst], src) {
      next_pc = pc + 1 + ins.off
    }
  } else if op == 0x06 {
    next_pc = pc + 1 + ins.off
  } else if is_jmp32(op) {
    check_reg(ins.dst)
    let src = if (op & 0x08) != 0 {
      check_reg(ins.src)
      vm.regs[ins.src]
    } else {
      imm_u64(ins.imm)
    }
    if jump_taken32(op, vm.regs[ins.dst], src) {
      next_pc = pc + 1 + ins.off
    }
  } else if op == 0x85 {
    if ins.src == 1 {
      if call_stack.length() >= max_call_depth {
        raise CallDepthExceeded
      }
      call_stack.push(pc + 1)
      call_saved.push(vm.regs[6])
      call_saved.push(vm.regs[7])
      call_saved.push(vm.regs[8])
      call_saved.push(vm.regs[9])
      next_pc = pc + 1 + ins.imm
    } else if ins.src != 0 {
      raise UnsupportedCallMode(ins.src)
    } else if ins.imm == HELPER_SUM {
      check_reg(1)
      check_reg(2)
      let len = vm.regs[2].to_int()
      if len < 0 || len > 1048576 {
        raise MemoryAccess(vm.regs[1], len)
      }
      vm.regs[0] = helper_sum(vm, vm.regs[1], len)
    } else if ins.imm == HELPER_MAP_LOOKUP {
      check_reg(1)
      check_reg(2)
      vm.regs[0] = helper_map_lookup(
        vm, vm.regs[1].to_int(), vm.regs[2],
      )
    } else if ins.imm == HELPER_MAP_UPDATE {
      check_reg(1)
      check_reg(2)
      check_reg(3)
      check_reg(4)
      vm.regs[0] = helper_map_update(
        vm, vm.regs[1].to_int(), vm.regs[2], vm.regs[3], vm.regs[4].to_int(),
      )
    } else if ins.imm == HELPER_MAP_DELETE {
      check_reg(1)
      check_reg(2)
      vm.regs[0] = helper_map_delete(
        vm, vm.regs[1].to_int(), vm.regs[2],
      )
    } else {
      raise UnknownHelper(ins.imm)
    }
  } else if op == 0x95 {
    if call_stack.length() > 0 {
      let ret = match call_stack.pop() {
        Some(value) => value
        None => pc + 1
      }
      vm.regs[9] = match call_saved.pop() {
        Some(value) => value
        None => vm.regs[9]
      }
      vm.regs[8] = match call_saved.pop() {
        Some(value) => value
        None => vm.regs[8]
      }
      vm.regs[7] = match call_saved.pop() {
        Some(value) => value
        None => vm.regs[7]
      }
      vm.regs[6] = match call_saved.pop() {
        Some(value) => value
        None => vm.regs[6]
      }
      return (ret, None)
    }
    return (pc, Some(vm.regs[0]))
  } else if op == 0xd4 || op == 0xdc || op == 0xd7 {
    check_dst(ins.dst)
    vm.regs[ins.dst] = endian_op(op, vm.regs[ins.dst], ins.imm)
  } else if op == 0x18 {
    check_dst(ins.dst)
    if ins.src != 0 {
      raise UnsupportedMapFd
    }
    if pc + 1 >= program.length() {
      raise InvalidProgram(pc)
    }
    let hi = program[pc + 1].imm
    vm.regs[ins.dst] = low32(imm_u64(ins.imm)).lor(
      low32(imm_u64(hi)).shl(32),
    )
    next_pc = pc + 2
  } else {
    raise UnknownOpcode(op)
  }
  if next_pc < 0 || next_pc > program.length() {
    raise InvalidProgram(next_pc)
  }
  (next_pc, None)
}

pub fn Machine::step(self : Machine) -> StepResult raise BpfError {
  if self.halted {
    return Halted(self.retval)
  }
  if self.pc < 0 || self.pc >= self.program.length() {
    raise InvalidProgram(self.pc)
  }
  self.steps = self.steps + 1
  if self.steps > self.max_steps {
    raise StepBudgetExceeded
  }
  while self.executed.length() <= self.pc {
    self.executed.push(0)
  }
  self.executed[self.pc] = self.executed[self.pc] + 1
  let (next, halt) = step_once(
    self.program,
    self.vm,
    self.pc,
    self.call_stack,
    self.call_saved,
    self.max_call_depth,
  )
  self.pc = next
  match halt {
    Some(value) => {
      self.halted = true
      self.retval = value
      Halted(value)
    }
    None => Continue
  }
}

pub fn Machine::run(self : Machine) -> UInt64 raise BpfError {
  let mut done = false
  let mut result = 0UL
  while !done {
    match self.step() {
      Halted(value) => {
        result = value
        done = true
      }
      Continue => ()
      BreakpointHit(_) => ()
    }
  }
  result
}

pub fn Machine::run_n(self : Machine, limit : Int) -> StepResult raise BpfError {
  let mut count = 0
  while count < limit && !self.halted {
    ignore(self.step())
    count = count + 1
  }
  if self.halted {
    Halted(self.retval)
  } else {
    Continue
  }
}

pub fn Machine::run_until_break(self : Machine) -> StepResult raise BpfError {
  let mut done = false
  let mut result = BreakpointHit(self.pc)
  while !done {
    if !self.halted && self.is_breakpoint(self.pc) {
      result = BreakpointHit(self.pc)
      done = true
    } else {
      match self.step() {
        Halted(value) => {
          result = Halted(value)
          done = true
        }
        Continue => ()
        BreakpointHit(pc) => {
          result = BreakpointHit(pc)
          done = true
        }
      }
    }
  }
  result
}