// =======================================================
// CastInst
// =======================================================

///|
pub(all) enum CastOps {
  Trunc
  ZExt
  SExt
  FPTrunc
  FPExt
  UIToFP
  SIToFP
  FPToUI
  FPToSI
  PtrToInt
  IntToPtr
  BitCast
  //AddrSpaceCast
} derive(Hash, Eq)

///|
pub impl Show for CastOps with output(self, logger) {
  let str = match self {
    Trunc => "trunc"
    ZExt => "zext"
    SExt => "sext"
    FPTrunc => "fptrunc"
    FPExt => "fpext"
    UIToFP => "uitofp"
    SIToFP => "sitofp"
    FPToUI => "fptoui"
    FPToSI => "fptosi"
    PtrToInt => "ptrtoint"
    IntToPtr => "inttoptr"
    BitCast => "bitcast"
    //AddrSpaceCast => "addrspacecast"
  }
  logger.write_string(str)
}

///|
/// CastInst represents a type conversion instruction that converts a value from one type to another.
///
/// **Note**:
///
/// Use `IRBuilder::createTrunc`, `IRBuilder::createZExt`, `IRBuilder::createSExt`, etc. to create cast instructions.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let i64_ty = ctx.getInt64Ty()
///   let f32_ty = ctx.getFloatTy()
///   let fty = ctx.getFunctionType(i32_ty, [i64_ty])
///   let fval = mod.addFunction(fty, "cast_demo")
///   let bb = fval.addBasicBlock(name="entry")
///   let arg = fval.getArg(0).unwrap()
///   builder.setInsertPoint(bb)
///   let trunc = builder.createTrunc(arg, i32_ty, name="truncated")
///   inspect(trunc, content="  %truncated = trunc i64 %0 to i32")
///   assert_true(trunc.asValueEnum() is CastInst(_))
///   let zext = builder.createZExt(trunc, i64_ty, name="extended")
///   inspect(zext, content="  %extended = zext i32 %truncated to i64")
///   let bitcast = builder.createBitCast(trunc, f32_ty, name="bits")
///   inspect(bitcast, content="  %bits = bitcast i32 %truncated to float")
/// }
/// ```
pub struct CastInst {
  uid : UInt64
  to_ty : &Type
  from_val : &Value
  mut name : String?
  parent : Function
  users : Array[&User]

  // --- InstBase ---
  bb : Ref[BasicBlock?]
  prev : Ref[&Instruction?]
  next : Ref[&Instruction?]
  opcode : CastOps
}

///|
fn CastInst::newTrunc(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  guard from_val.getType().tryAsIntTypeEnum() is Some(from_ty)
  guard from_ty.getBitWidth() > to_ty.getBitWidth()
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: Trunc,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newZExt(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  guard from_val.getType().tryAsIntTypeEnum() is Some(from_ty)
  guard from_ty.getBitWidth() < to_ty.getBitWidth()
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: ZExt,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newSExt(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: SExt,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newFPTrunc(
  from_val : &Value,
  to_ty : &FPType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: FPTrunc,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newFPExt(
  from_val : &Value,
  to_ty : &FPType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: FPExt,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newUIToFP(
  from_val : &Value,
  to_ty : &FPType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: UIToFP,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newFPToUI(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: FPToUI,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newSIToFP(
  from_val : &Value,
  to_ty : &FPType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: SIToFP,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newFPToSI(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: FPToSI,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newPtrToInt(
  from_val : &Value,
  to_ty : &IntegerType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: PtrToInt,
  }
  from_val.addUser(inst)
  inst
}

///|
fn CastInst::newIntToPtr(
  from_val : &Value,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let to_ty = from_val.getContext().getPtrTy()
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: IntToPtr,
  }
  from_val.addUser(inst)
  inst
}

///|
/// REVIEW: Currently bitcast is only allowed between primitive types.
/// while in real cpp llvm, bitcast can be used to cast between aggregate types.
fn CastInst::newBitCast(
  from_val : &Value,
  to_ty : &PrimitiveType,
  parent : Function,
  name~ : String?,
) -> CastInst {
  let uid = valueUIDAssigner.assign()
  let bb : Ref[BasicBlock?] = Ref::new(None)
  let prev : Ref[&Instruction?] = Ref::new(None)
  let next : Ref[&Instruction?] = Ref::new(None)
  let inst = CastInst::{
    uid,
    to_ty,
    from_val,
    name,
    parent,
    users: [],
    bb,
    prev,
    next,
    opcode: BitCast,
  }
  from_val.addUser(inst)
  inst
}

///|
pub impl Value for CastInst with getValueBase(self) {
  ValueBase::{ uid: self.uid, vty: self.to_ty, users: self.users }
}

///|
/// Get simple representation of the value.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let i64_ty = ctx.getInt64Ty()
///   let fty = ctx.getFunctionType(i32_ty, [i64_ty])
///   let fval = mod.addFunction(fty, "cast_demo")
///   let bb = fval.addBasicBlock(name="entry")
///   let arg = fval.getArg(0).unwrap()
///   builder.setInsertPoint(bb)
///   let trunc = builder.createTrunc(arg, i32_ty)
///   inspect(trunc.getValueRepr(), content="%1")
///   trunc.setName("truncated")
///   inspect(trunc.getValueRepr(), content="%truncated")
/// }
/// ```
pub impl Value for CastInst with getValueRepr(self) {
  match self.getNameOrSlot() {
    Some(Left(name)) => "%\{name}"
    Some(Right(slot)) => "%\{slot}"
    None => ""
  }
}

///|
pub impl Value for CastInst with asValueEnum(self) {
  CastInst(self)
}

///|
/// Get the name of the instruction.
///
/// **Note**:
///
/// If the instruction has no name, return `None`.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let i64_ty = ctx.getInt64Ty()
///   let fty = ctx.getFunctionType(i32_ty, [i64_ty])
///   let fval = mod.addFunction(fty, "cast_demo")
///   let bb = fval.addBasicBlock(name="entry")
///   let arg = fval.getArg(0).unwrap()
///   builder.setInsertPoint(bb)
///   let trunc = builder.createTrunc(arg, i32_ty)
///   inspect(trunc.getName(), content="None")
///   trunc.setName("truncated")
///   inspect(trunc.getName(), content="Some(\"truncated\")")
/// }
/// ```
pub impl Value for CastInst with getName(self) {
  self.name
}

///|
/// Set the name of the instruction.
///
/// **Note**:
///
/// If the name has already been used in the parent function,
/// it will raise Error.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let i64_ty = ctx.getInt64Ty()
///   let fty = ctx.getFunctionType(i32_ty, [i64_ty])
///   let fval = mod.addFunction(fty, "cast_demo")
///   let bb = fval.addBasicBlock(name="entry")
///   let arg = fval.getArg(0).unwrap()
///   builder.setInsertPoint(bb)
///   let trunc = builder.createTrunc(arg, i32_ty)
///   inspect(trunc.getName(), content="None")
///   trunc.setName("truncated")
///   inspect(trunc.getName(), content="Some(\"truncated\")")
/// }
/// ```
pub impl Value for CastInst with setName(self, name) {
  match self.getParent().setSymbol(name, self) {
    EmptyName => {
      let msg = "Misuse `CastInst::setName`: name cannot be empty."
      raise LLVMValueError(msg)
    }
    InvalidName => {
      let msg =
        $|Misuse `CastInst::setName`:
        $|name '\{name}' contains illegal characters,
        $|only alphanumeric characters and underscores are allowed
      raise LLVMValueError(msg)
    }
    DuplicateName(existed) => {
      let msg =
        $|Misuse `CastInst::setName`:
        $|name '\{name}' already exists in the parent function,
        $|it is used by:
        $|\{existed}"
      raise LLVMValueError(msg)
    }
    Success => self.name = Some(name)
  }
}

///|
pub impl Value for CastInst with removeName(self) {
  match self.name {
    None => ()
    Some(name) => {
      self.getParent().symbols.remove(name)
      self.name = None
    }
  }
}

///|
pub impl Value for CastInst with getNameOrSlot(self) {
  match self.name {
    Some(name) => Some(Left(name))
    None =>
      match self.getParent().getSlot(self) {
        Some(slot) => Some(Right(slot))
        None => None
      }
  }
}

///|
pub impl User for CastInst with asUserEnum(self) {
  CastInst(self)
}

///|
pub impl User for CastInst with getUserBase(self) {
  UserBase::{ operands: [self.from_val] }
}

///|
pub impl UnaryInst for CastInst with asUnaryInstEnum(self) {
  CastInst(self)
}

///|
pub impl Instruction for CastInst with getInstBase(self) {
  InstBase::{ bb: self.bb, prev: self.prev, next: self.next }
}

///|
pub impl Instruction for CastInst with asInstEnum(self) {
  CastInst(self)
}

///|
pub impl Instruction for CastInst with getParent(self) {
  self.parent
}

///|
pub impl Show for CastInst with output(self, logger) {
  let repr = self.getValueRepr()
  let to_ty = self.to_ty
  let from_ty = self.from_val.getType()
  let from_val_repr = self.from_val.getValueRepr()
  logger.write_string(
    "  \{repr} = \{self.opcode} \{from_ty} \{from_val_repr} to \{to_ty}",
  )
}