///|
#derive(Debug)
pub suberror X64LowerError {
  UnsupportedOperation(
    block_index~ : Int32,
    instruction_index~ : Int32,
    operation~ : @lowering.Operation
  )
  UnsupportedAbi(message~ : String)
  BuildFailure(cause~ : @vcode.VCodeBuildError)
  InvalidTarget(cause~ : TargetVCodeVerifyError)
}

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

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

///|
fn gpr_width(ty : @native.ValueType) -> GprWidth? {
  match ty {
    I32 => Some(W32)
    I64 => Some(W64)
    _ => None
  }
}

///|
fn optional_gpr_width(ty : @native.ValueType?) -> GprWidth? {
  match ty {
    Some(ty) => gpr_width(ty)
    None => None
  }
}

///|
fn scalar_access_width(ty : @native.ValueType) -> @native.AccessWidth? {
  match ty {
    I32 | F32 => Some(W32)
    I64 | Ptr64 | GcRef64 | F64 => Some(W64)
    V128 => None
  }
}

///|
fn lower_binary(operation : @lowering.IntBinaryOp) -> X64IntBinary? {
  match operation {
    Add => Some(Add)
    Sub => Some(Sub)
    Mul => Some(Mul)
    And => Some(And)
    Or => Some(Orr)
    Xor => Some(Eor)
    ShiftLeft => Some(Lsl)
    SignedShiftRight => Some(Asr)
    UnsignedShiftRight => Some(Lsr)
    RotateRight => Some(Ror)
    SignedDiv | UnsignedDiv | SignedRem | UnsignedRem | RotateLeft => None
  }
}

///|
fn lower_conversion(conversion : @lowering.ConversionOp) -> X64Conversion? {
  match conversion {
    I32WrapI64 => Some(WrapI64ToI32)
    I64ExtendI32(signedness) => Some(ExtendI32ToI64(signedness))
    SignExtend(I32, W8) => Some(SignExtend(I32, W8))
    SignExtend(I32, W16) => Some(SignExtend(I32, W16))
    SignExtend(I64, W8) => Some(SignExtend(I64, W8))
    SignExtend(I64, W16) => Some(SignExtend(I64, W16))
    SignExtend(I64, W32) => Some(SignExtend(I64, W32))
    F32DemoteF64 => Some(DemoteF64ToF32)
    F64PromoteF32 => Some(PromoteF32ToF64)
    Bitcast(from, to) => Some(Bitcast(from, to))
    IntToFloat(from, to, signedness) => Some(IntToFloat(from, to, signedness))
    FloatToInt(_, _, _, _) | SignExtend(_, _) => None
  }
}

///|
fn append_body(
  builder : @vcode.CheckedBuilder[X64Inst],
  block : @vcode.Block,
  instruction : X64Inst,
  inputs : Array[@vcode.Input],
  outputs : Array[@vcode.Output],
  metadata : @vcode.InstructionMetadata,
  clobbers~ : Array[@vcode.PhysicalReg] = [],
) -> Array[@vcode.Value] raise X64LowerError {
  // The instruction's own encoding constraints are authoritative; a caller may
  // only add to them. See `X64Inst::mandatory_clobbers`.
  let effective_clobbers = clobbers.copy()
  for reg in instruction.mandatory_clobbers() {
    if !effective_clobbers.contains(reg) {
      effective_clobbers.push(reg)
    }
  }
  let (_, results) = builder.append_body(
    block, instruction, inputs, outputs, effective_clobbers, metadata,
  ) catch {
    error => raise BuildFailure(cause=error)
  }
  results
}

///|
fn lower_call_results(
  builder : @vcode.CheckedBuilder[X64Inst],
  block : @vcode.Block,
  result_types : Array[@native.ValueType],
  locations : ArrayView[CallResultLocation],
  metadata : @vcode.InstructionMetadata,
) -> Array[@vcode.Value] raise X64LowerError {
  @vcode.materialize_call_results(
    builder,
    block,
    result_types,
    locations,
    metadata.live_gc_roots,
    (ty, reg) => IncomingCallResult(ty, reg),
    (ty, offset) => IncomingCallAreaResult(ty, offset),
    KeepAlive(GcRef64),
    is_allocatable,
  ) catch {
    error => raise BuildFailure(cause=error)
  }
}

///|
fn lower_call(
  context : LoweringContext,
  builder : @vcode.CheckedBuilder[X64Inst],
  block : @vcode.Block,
  call : @native.NativeCall,
  operands : Array[@vcode.Value],
  result_types : Array[@native.ValueType],
  metadata : @vcode.InstructionMetadata,
) -> Array[@vcode.Value] raise X64LowerError {
  let (instruction, inputs, clobbers, locations) : (
    X64Inst,
    Array[@vcode.Input],
    Array[@vcode.PhysicalReg],
    ArrayView[CallResultLocation],
  ) = match call.protocol {
    Platform => {
      if result_types.length() > 1 {
        raise UnsupportedAbi(
          message="platform calls support at most one direct result",
        )
      }
      let target = match call.callee {
        External(symbol) => symbol
        _ =>
          raise UnsupportedAbi(
            message="platform calls require a direct external symbol",
          )
      }
      let operands = builder.materialize_abi_words(
        block,
        operands,
        call.signature.params,
        ty => Move(ty),
      ) catch {
        error => raise BuildFailure(cause=error)
      }
      let result_registers = platform_result_registers(call.signature.results)
      let inputs = @vcode.call_argument_inputs(
        operands,
        platform_call_layout(call.signature.params).arguments,
        is_allocatable,
      )
      let instruction = if call.behavior.returns_twice {
        ReturnsTwicePlatformCall(target, call.signature)
      } else {
        PlatformCall(target, call.signature)
      }
      (
        instruction,
        inputs,
        platform_call_clobbers(),
        result_registers.map(reg => CallResultRegister(reg)),
      )
    }
    Internal => {
      if call.behavior.returns_twice {
        raise UnsupportedAbi(
          message="returns-twice internal calls are not supported",
        )
      }
      let target = match call.callee {
        Internal(symbol) => Some(symbol)
        Indirect => None
        External(_) =>
          raise UnsupportedAbi(
            message="internal calls require a code symbol or function pointer",
          )
      }
      let operands = builder.materialize_abi_words(
        block,
        operands,
        call.signature.params,
        ty => Move(ty),
        operand_start=if target is None { 1 } else { 0 },
      ) catch {
        error => raise BuildFailure(cause=error)
      }
      let plan = context.internal_abi.call_plan(call.signature) catch {
        error => raise UnsupportedAbi(message=error.to_string())
      }
      let instruction = match target {
        Some(target) => InternalCall(target, call.signature, plan)
        None => InternalCallIndirect(call.signature, plan)
      }
      let inputs = match target {
        Some(_) =>
          @vcode.call_argument_inputs(operands, plan.arguments, is_allocatable)
        None =>
          [
            @vcode.Input::any_location(operands[0]),
            ..@vcode.call_argument_inputs(
              operands[1:].to_owned(),
              plan.arguments,
              is_allocatable,
            ),
          ]
      }
      (instruction, inputs, internal_call_clobbers(plan), plan.results)
    }
  }
  ignore(
    builder.append_body(block, instruction, inputs, [], clobbers, metadata) catch {
      error => raise BuildFailure(cause=error)
    },
  )
  lower_call_results(builder, block, result_types, locations, metadata)
}

///|
fn set_terminator(
  builder : @vcode.CheckedBuilder[X64Inst],
  block : @vcode.Block,
  instruction : X64Inst,
  inputs : Array[@vcode.Input],
  successors : Array[@vcode.Edge],
  metadata : @vcode.InstructionMetadata,
  clobbers~ : Array[@vcode.PhysicalReg] = [],
) -> Unit raise X64LowerError {
  (builder.set_terminator(
    block, instruction, inputs, successors, clobbers, metadata,
  )
  |> ignore) catch {
    error => raise BuildFailure(cause=error)
  }
}

///|
priv struct ConstructedFunction {
  function : @vcode.Function[X64Inst]
}

///|
fn validate_constructed(
  constructed : ConstructedFunction,
  on_event : (@vcode.TargetCompileEvent) -> Unit,
) -> Unit raise X64LowerError {
  on_event(TargetValidationStarted)
  on_event(TargetCommonValidationStarted)
  @vcode.verify_selected(constructed.function) catch {
    error => raise InvalidTarget(cause=CommonFailure(cause=error))
  }
  on_event(TargetIsaValidationStarted)
  verify_target_vcode(constructed.function) catch {
    error => raise InvalidTarget(cause=error)
  }
}

///|
/// Opaque evidence that direct x64 selection and VCode validation completed.
pub struct SelectedFunction {
  priv constructed : ConstructedFunction
}

///|
/// Select and validate x64 VCode for immediate target compilation.