///|
/// Register assignment shared by the native backends' internal calls.
pub struct InternalAbi {
  priv context_argument : PhysicalReg
  result_area_argument : PhysicalReg
  priv integer_arguments : Array[PhysicalReg]
  priv fp_arguments : Array[PhysicalReg]
  priv integer_results : Array[PhysicalReg]
  priv fp_results : Array[PhysicalReg]
}

///|
#derive(Eq, Debug)
pub struct InternalCallPlan {
  arguments : ArrayView[CallArgumentLocation]
  results : ArrayView[CallResultLocation]
  stack_size : Int32
  result_area_size : Int32
}

///|
#derive(Eq, Debug)
pub suberror InternalAbiError {
  InvalidRegister(role~ : String, index~ : Int32)
  DuplicateRegister(role~ : String, index~ : Int32)
  ContextRegisterReused(index~ : Int32)
  ResultAreaRegisterReused(index~ : Int32)
  InvalidSignature(message~ : String)
}

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

///|
fn checked_registers(
  role : String,
  indices : Array[Int32],
  class : RegClass,
) -> Array[PhysicalReg] raise InternalAbiError {
  let registers : Array[PhysicalReg] = []
  for index in indices {
    if index < 0 || index >= 16 {
      raise InvalidRegister(role~, index~)
    }
    if registers.contains(PhysicalReg::new(index, class)) {
      raise DuplicateRegister(role~, index~)
    }
    registers.push(PhysicalReg::new(index, class))
  }
  registers
}

///|
pub fn InternalAbi::new(
  context_argument : Int32,
  result_area_argument : Int32,
  integer_arguments : Array[Int32],
  fp_arguments : Array[Int32],
  integer_results : Array[Int32],
  fp_results : Array[Int32],
) -> InternalAbi raise InternalAbiError {
  if context_argument < 0 || context_argument >= 16 {
    raise InvalidRegister(role="context argument", index=context_argument)
  }
  let context = PhysicalReg::new(context_argument, Int)
  if result_area_argument < 0 || result_area_argument >= 16 {
    raise InvalidRegister(
      role="result area argument",
      index=result_area_argument,
    )
  }
  let result_area = PhysicalReg::new(result_area_argument, Int)
  let integer_arguments = checked_registers(
    "integer argument",
    integer_arguments,
    Int,
  )
  if integer_arguments.contains(context) {
    raise ContextRegisterReused(index=context_argument)
  }
  if result_area == context || integer_arguments.contains(result_area) {
    raise ResultAreaRegisterReused(index=result_area_argument)
  }
  let integer_results = checked_registers(
    "integer result",
    integer_results,
    Int,
  )
  if integer_results.contains(result_area) {
    raise ResultAreaRegisterReused(index=result_area_argument)
  }
  {
    context_argument: context,
    result_area_argument: result_area,
    integer_arguments,
    fp_arguments: checked_registers("FP argument", fp_arguments, FpVector),
    integer_results,
    fp_results: checked_registers("FP result", fp_results, FpVector),
  }
}

///|
pub fn InternalAbi::call_layout(
  self : InternalAbi,
  signature : @native.Signature,
) -> CallLayout raise InternalAbiError {
  if signature.params.is_empty() || signature.params[0] != Ptr64 {
    raise InvalidSignature(
      message="internal signature must start with an explicit ptr64 execution environment",
    )
  }
  let mut integer_count = 0
  for index in 1.. self.integer_arguments.length() {
    integer_count - self.integer_arguments.length()
  } else {
    0
  }
  let arguments : Array[CallArgumentLocation] = [
    CallRegister(self.context_argument),
  ]
  let mut integer_index = 0
  let mut fp_index = 0
  let mut integer_stack_index = 0
  let mut fp_stack_offset = integer_overflow * 8
  for index in 1.. (ArrayView[CallResultLocation], Int32) {
  let locations : Array[CallResultLocation] = []
  let mut integer_index = 0
  let mut fp_index = 0
  let mut integer_area_offset = 0
  for ty in result_types {
    if reg_class_for_value_type(ty) == Int &&
      integer_index >= self.integer_results.length() {
      integer_area_offset += 8
    }
    if reg_class_for_value_type(ty) == Int {
      integer_index += 1
    }
  }
  integer_index = 0
  let mut fp_area_offset = integer_area_offset
  for ty in result_types {
    if reg_class_for_value_type(ty) == Int {
      if integer_index < self.integer_results.length() {
        locations.push(CallResultRegister(self.integer_results[integer_index]))
      } else {
        locations.push(
          CallResultArea(
            (integer_index - self.integer_results.length()) * 8,
            self.result_area_argument,
          ),
        )
      }
      integer_index += 1
    } else {
      if fp_index < self.fp_results.length() {
        locations.push(
          CallResultRegister(
            PhysicalReg::new(
              self.fp_results[fp_index].id,
              reg_class_for_value_type(ty),
            ),
          ),
        )
      } else {
        let size = call_stack_size(ty)
        fp_area_offset = call_align_up(fp_area_offset, size)
        locations.push(
          CallResultArea(fp_area_offset, self.result_area_argument),
        )
        fp_area_offset += size
      }
      fp_index += 1
    }
  }
  (locations[:], call_align_up(fp_area_offset, 16))
}

///|
pub fn InternalAbi::call_plan(
  self : InternalAbi,
  signature : @native.Signature,
) -> InternalCallPlan raise InternalAbiError {
  let layout = self.call_layout(signature)
  let (results, result_area_size) = self.result_layout(signature.results)
  {
    arguments: layout.arguments,
    results,
    stack_size: layout.stack_size,
    result_area_size,
  }
}

///|
#derive(Eq, Debug)
pub(all) enum CallArgumentLocation {
  CallRegister(PhysicalReg)
  CallStack(Int32)
}

///|
#derive(Eq, Debug)
pub struct CallLayout {
  arguments : ArrayView[CallArgumentLocation]
  stack_size : Int32
}

///|
#derive(Eq, Debug)
pub(all) enum CallResultLocation {
  CallResultRegister(PhysicalReg)
  CallResultArea(Int32, PhysicalReg)
}

///|
fn call_align_up(value : Int32, alignment : Int32) -> Int32 {
  (value + alignment - 1) / alignment * alignment
}

///|
pub fn call_stack_size(ty : @native.ValueType) -> Int32 {
  if ty == V128 {
    16
  } else {
    8
  }
}

///|
pub fn InternalCallPlan::valid_results(
  plan : InternalCallPlan,
  types : Array[@native.ValueType],
) -> Bool {
  if plan.results.length() != types.length() ||
    plan.result_area_size < 0 ||
    plan.result_area_size % 16 != 0 {
    return false
  }
  let mut area_end = 0
  let mut area_register : PhysicalReg? = None
  for index, ty in types {
    match plan.results[index] {
      CallResultRegister(reg) =>
        if reg.id < 0 ||
          reg.id >= 16 ||
          reg.class != reg_class_for_value_type(ty) {
          return false
        }
      CallResultArea(offset, reg) => {
        let size = call_stack_size(ty)
        if offset < 0 ||
          offset % size != 0 ||
          reg.id < 0 ||
          reg.id >= 16 ||
          reg.class != Int {
          return false
        }
        match area_register {
          Some(expected) if expected != reg => return false
          None => area_register = Some(reg)
          _ => ()
        }
        let end = offset + size
        if end > area_end {
          area_end = end
        }
      }
    }
  }
  call_align_up(area_end, 16) == plan.result_area_size
}

///|
/// Lay out platform arguments with the target's integer registers and eight FP slots.
pub fn platform_call_layout(
  types : Array[@native.ValueType],
  integer_registers : ArrayView[Int32],
) -> CallLayout {
  let arguments : Array[CallArgumentLocation] = []
  let mut int_index = 0
  let mut fp_index = 0
  let mut stack_size = 0
  for ty in types {
    let class = reg_class_for_value_type(ty)
    let index = if class == Int {
      let current = int_index
      int_index += 1
      current
    } else {
      let current = fp_index
      fp_index += 1
      current
    }
    let register_capacity = if class == Int {
      integer_registers.length()
    } else {
      8
    }
    if index < register_capacity {
      arguments.push(
        CallRegister(
          PhysicalReg::new(
            if class == Int {
              integer_registers[index]
            } else {
              index
            },
            class,
          ),
        ),
      )
    } else {
      let alignment = call_stack_size(ty)
      stack_size = call_align_up(stack_size, alignment)
      arguments.push(CallStack(stack_size))
      stack_size += alignment
    }
  }
  { arguments: arguments[:], stack_size: call_align_up(stack_size, 16), }
}

///|
pub fn InternalCallPlan::result_area_register(
  plan : InternalCallPlan,
) -> PhysicalReg? {
  for result in plan.results {
    if result is CallResultArea(_, reg) {
      return Some(reg)
    }
  }
  None
}