///|
pub suberror X64LinkError {
  InvalidCodeObject(message~ : String)
  InvalidCodeBase(address~ : Int64)
  UnresolvedRelocation(index~ : Int)
  RelocationOutOfRange(index~ : Int)
  UnsupportedRelocation(index~ : Int)
} derive(Eq, Debug)

///|
pub impl Show for X64LinkError with fn output(self, logger) {
  logger.write_string(Repr(self).to_string())
}

///|
pub struct X64LinkPlan {
  priv code : Array[Byte]
  priv relocations : Array[@code_object.Relocation]
  priv veneer_offsets : Array[Int]
} derive(Debug)

///|
pub fn X64LinkPlan::code(self : X64LinkPlan) -> Array[Byte] {
  self.code.copy()
}

///|
pub fn X64LinkPlan::veneer_offsets(self : X64LinkPlan) -> Array[Int] {
  self.veneer_offsets.copy()
}

///|
fn append_call_veneer(code : Array[Byte]) -> Int {
  let offset = code.length()
  // jmp qword ptr [rip + 0], followed by the absolute target address.
  code.append([
    b'\xff', b'\x25', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
    b'\x00', b'\x00', b'\x00', b'\x00', b'\x00',
  ])
  offset
}

///|
pub fn prepare_x64_link(
  object : @code_object.UnlinkedCodeObject,
) -> X64LinkPlan raise X64LinkError {
  object.verify() catch {
    error => raise InvalidCodeObject(message=error.to_string())
  }
  if object.architecture() != X64 {
    raise InvalidCodeObject(message="expected an x64 code object")
  }
  let code = object.code()
  let relocations = object.relocations()
  let veneer_offsets = Array::make(relocations.length(), -1)
  let veneer_targets : Array[@code_object.RelocationTarget] = []
  let veneer_addends : Array[Int64] = []
  let veneers : Array[Int] = []
  for index, relocation in relocations {
    if relocation.kind is X64PcRelative32 {
      let mut veneer = -1
      for target_index, target in veneer_targets {
        if target == relocation.target &&
          veneer_addends[target_index] == relocation.addend {
          veneer = veneers[target_index]
          break
        }
      }
      if veneer < 0 {
        veneer = append_call_veneer(code)
        veneer_targets.push(relocation.target)
        veneer_addends.push(relocation.addend)
        veneers.push(veneer)
      }
      veneer_offsets[index] = veneer
    }
  }
  { code, relocations, veneer_offsets }
}

///|
fn write_u32(code : Array[Byte], offset : Int, value : UInt) -> Unit {
  for byte in 0..<4 {
    code[offset + byte] = ((value >> (byte * 8)) & 0xFFU).to_byte()
  }
}

///|
fn write_address(code : Array[Byte], offset : Int, address : Int64) -> Unit {
  let bits = address.reinterpret_as_uint64()
  for byte in 0..<8 {
    code[offset + byte] = ((bits >> (byte * 8)) & 0xFFUL).to_byte()
  }
}

///|
pub fn X64LinkPlan::link(
  self : X64LinkPlan,
  code_address : Int64,
  resolve : (@code_object.RelocationTarget) -> Int64?,
) -> Array[Byte] raise X64LinkError {
  if code_address < 0L {
    raise InvalidCodeBase(address=code_address)
  }
  let code = self.code.copy()
  for index, relocation in self.relocations {
    guard resolve(relocation.target) is Some(resolved) else {
      raise UnresolvedRelocation(index~)
    }
    let target = resolved + relocation.addend
    match relocation.kind {
      Absolute64 => write_address(code, relocation.offset, target)
      X64PcRelative32 => {
        let next_instruction = code_address + relocation.offset.to_int64() + 4L
        let displacement = target - next_instruction
        if displacement >= -0x80000000L && displacement <= 0x7FFFFFFFL {
          write_u32(
            code,
            relocation.offset,
            displacement.to_int().reinterpret_as_uint(),
          )
        } else {
          let veneer = self.veneer_offsets[index]
          if veneer < 0 {
            raise RelocationOutOfRange(index~)
          }
          let veneer_displacement = veneer.to_int64() -
            relocation.offset.to_int64() -
            4L
          if veneer_displacement < -0x80000000L ||
            veneer_displacement > 0x7FFFFFFFL {
            raise RelocationOutOfRange(index~)
          }
          write_u32(
            code,
            relocation.offset,
            veneer_displacement.to_int().reinterpret_as_uint(),
          )
          write_address(code, veneer + 6, target)
        }
      }
      AArch64Call26 | AArch64Jump26 | AArch64Page21 | AArch64PageOffset12 =>
        raise UnsupportedRelocation(index~)
    }
  }
  code
}