///|
priv struct FrameStackObjects {
  offset : Int32
  size : Int32
  alignment : Int32
}

///|
priv struct FrameResultArea {
  offset : Int32
  size : Int32
}

///|
/// The local frame areas shared by targets. Incoming arguments, tail-call
/// staging and frame records are planned by the target ABI.
pub struct FrameStorage {
  priv local_size : Int32
  priv result_area : FrameResultArea?
  priv stack_objects : FrameStackObjects?
  priv stack_object_offsets : Array[Int32]
  priv emergency_move_offset : Int32?
  priv requires_emergency_moves : Bool
  priv slots : Array[(StackSlot, Int32)]
  priv saved_registers : Array[(PhysicalReg, Int32)]
}

///|
pub(all) suberror FrameStorageError {
  InvalidSavedRegister(reg~ : PhysicalReg, offset~ : Int32)
  InvalidStackSlot(slot~ : StackSlot)
  ResultAreaMismatch(expected~ : Int32, actual~ : Int32)
  InvalidStackObjectArea(message~ : String)
  InvalidEmergencyMoveArea(offset~ : Int32?)
  SavedRegisterSetMismatch
}

///|
pub fn FrameStorage::plan(
  requirements : FrameRequirements,
  allocation : Allocation,
  requires_emergency_moves : Bool,
  saved : Array[PhysicalReg],
) -> FrameStorage {
  let outgoing_args_size = requirements.outgoing_args_size
  let mut offset = outgoing_args_size
  let result_area_size = requirements.result_area_size
  let result_area = if result_area_size > 0 {
    offset = align_frame_area(offset, 16)
    let area = { offset, size: result_area_size, }
    offset += result_area_size
    Some(area)
  } else {
    None
  }
  let stack_object_requirements = requirements.stack_objects
  let stack_objects = match stack_object_requirements {
    Some(requirements) => {
      offset = align_frame_area(offset, requirements.alignment)
      let area = {
        offset,
        size: requirements.size,
        alignment: requirements.alignment,
      }
      offset += requirements.size
      Some(area)
    }
    None => None
  }
  let emergency_move_offset = if requires_emergency_moves {
    offset = align_frame_area(offset, 16)
    let emergency_offset = offset
    offset += 16
    Some(emergency_offset)
  } else {
    None
  }
  let slots = Array::makei(allocation.stack_slot_count(), index => {
    let slot = allocation.stack_slot_at(index).unwrap()
    offset = align_frame_area(
      offset,
      allocation.stack_slot_alignment(slot).unwrap(),
    )
    let placement = (slot, offset)
    offset += allocation.stack_slot_size(slot).unwrap()
    placement
  })
  let saved_registers = saved.map(reg => {
    let size = 8
    offset = align_frame_area(offset, size)
    let placement = (reg, offset)
    offset += size
    placement
  })
  {
    local_size: align_frame_area(offset, 16),
    result_area,
    stack_objects,
    stack_object_offsets: stack_object_requirements
    .map(req => req.offsets)
    .unwrap_or([]),
    emergency_move_offset,
    requires_emergency_moves,
    slots,
    saved_registers,
  }
}

///|
pub fn FrameStorage::local_size(self : FrameStorage) -> Int32 {
  self.local_size
}

///|
pub fn FrameStorage::result_area_offset(self : FrameStorage) -> Int32? {
  self.result_area.map(area => area.offset)
}

///|
pub fn FrameStorage::stack_object_offset(
  self : FrameStorage,
  id : Int32,
) -> Int32? {
  match self.stack_objects {
    Some(area) if id >= 0 && id < self.stack_object_offsets.length() =>
      Some(area.offset + self.stack_object_offsets[id])
    _ => None
  }
}

///|
pub fn FrameStorage::emergency_move_offset(self : FrameStorage) -> Int32? {
  self.emergency_move_offset
}

///|
pub fn FrameStorage::saved_registers(
  self : FrameStorage,
) -> Array[(PhysicalReg, Int32)] {
  self.saved_registers.copy()
}

///|
pub fn FrameStorage::place_slots(
  self : FrameStorage,
  layout : FrameLayout,
) -> Unit {
  for placement in self.slots {
    let (slot, offset) = placement
    ignore(layout.place_slot(slot, offset))
  }
}

///|
pub fn FrameStorage::verify(
  self : FrameStorage,
  layout : FrameLayout,
  allocation : Allocation,
  requirements : FrameRequirements,
  expected_saved : Array[PhysicalReg],
) -> Unit raise FrameStorageError {
  let expected_result_area = requirements.result_area_size
  let actual_result_area = self.result_area.map(area => area.size).unwrap_or(0)
  if actual_result_area != expected_result_area {
    raise ResultAreaMismatch(
      expected=expected_result_area,
      actual=actual_result_area,
    )
  }
  if self.result_area is Some(area) &&
    (
      area.offset < requirements.outgoing_args_size ||
      area.offset % 16 != 0 ||
      area.offset + area.size > self.local_size
    ) {
    raise ResultAreaMismatch(expected=expected_result_area, actual=area.size)
  }
  let expected_stack_objects = requirements.stack_objects
  let reserved_call_area_end = match self.result_area {
    Some(area) => area.offset + area.size
    None => requirements.outgoing_args_size
  }
  match (expected_stack_objects, self.stack_objects) {
    (None, None) => ()
    (Some(requirements), Some(area)) =>
      if area.size != requirements.size ||
        area.alignment != requirements.alignment ||
        area.offset < reserved_call_area_end ||
        area.offset % requirements.alignment != 0 ||
        area.offset + requirements.size > self.local_size ||
        self.stack_object_offsets != requirements.offsets {
        raise InvalidStackObjectArea(
          message="planned stack-object area does not satisfy target requirements",
        )
      }
    _ =>
      raise InvalidStackObjectArea(
        message="planned stack-object area presence does not match target VCode",
      )
  }
  let reserved_object_area_end = match self.stack_objects {
    Some(area) => area.offset + area.size
    None => reserved_call_area_end
  }
  match (self.requires_emergency_moves, self.emergency_move_offset) {
    (false, None) => ()
    (true, Some(offset)) =>
      if offset < reserved_object_area_end ||
        offset % 16 != 0 ||
        offset + 16 > self.local_size {
        raise InvalidEmergencyMoveArea(offset=Some(offset))
      }
    _ => raise InvalidEmergencyMoveArea(offset=self.emergency_move_offset)
  }
  if self.saved_registers.map(saved => saved.0) != expected_saved {
    raise SavedRegisterSetMismatch
  }
  let occupied : Array[(Int32, Int32)] = []
  if requirements.outgoing_args_size > 0 {
    occupied.push((0, requirements.outgoing_args_size))
  }
  if self.result_area is Some(area) {
    occupied.push((area.offset, area.offset + area.size))
  }
  if self.stack_objects is Some(area) {
    occupied.push((area.offset, area.offset + area.size))
  }
  if self.emergency_move_offset is Some(offset) {
    occupied.push((offset, offset + 16))
  }
  for index in 0.. self.local_size {
      raise InvalidStackSlot(slot~)
    }
    occupied.push((offset, offset + allocation.stack_slot_size(slot).unwrap()))
  }
  for saved in self.saved_registers {
    let (reg, offset) = saved
    let size = 8
    if offset < 0 || offset % size != 0 || offset + size > self.local_size {
      raise InvalidSavedRegister(reg~, offset~)
    }
    for range in occupied {
      if offset < range.1 && range.0 < offset + size {
        raise InvalidSavedRegister(reg~, offset~)
      }
    }
    occupied.push((offset, offset + size))
  }
}

///|
pub fn FrameStorage::summary(
  self : FrameStorage,
  outgoing_args_size : Int32,
) -> String {
  let mut output = ""
  if outgoing_args_size > 0 {
    output += "  outgoing \{outgoing_args_size} bytes at sp+0\n"
  }
  if self.result_area is Some(area) {
    output += "  call-results \{area.size} bytes at sp+\{area.offset}\n"
  }
  if self.stack_objects is Some(area) {
    output += "  stack-objects \{area.size} bytes at sp+\{area.offset}\n"
  }
  if self.emergency_move_offset is Some(offset) {
    output += "  emergency-move 16 bytes at sp+\{offset}\n"
  }
  for entry in self.slots {
    let (slot, offset) = entry
    output += "  place \{Repr(slot)} at sp+\{offset}\n"
  }
  for saved in self.saved_registers {
    output += "  save \{Repr(saved.0)} at sp+\{saved.1}\n"
  }
  output
}