// =======================================================
// PHINode
// =======================================================

///|
/// PHINode represents a PHI node instruction that selects a value based on the predecessor basic block.
///
/// **Note**:
///
/// Use `IRBuilder::createPHI` to create a `PHINode`, then use `PHINode::addIncoming` to add incoming values.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block_bb = fval.addBasicBlock(name="block")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block_bb)
///   inspect(phi, content="  %result = phi i32 [ 10, %entry ], [ 20, %block ]")
///   assert_true(phi.asValueEnum() is PHINode(_))
/// }
/// ```
pub struct PHINode {
  uid : UInt64
  vty : &Type
  users : Array[&User]
  mut name : String?
  incomings : Array[(&Value, BasicBlock)]
  parent : Function

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

///|
fn PHINode::new(vty : &Type, parent : Function, name~ : String?) -> PHINode {
  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)
  PHINode::{ uid, vty, users: [], name, incomings: [], parent, bb, prev, next }
}

///|
/// Get the number of incoming values in the PHI node.
///
/// **Note**:
///
/// Returns the total count of incoming value-block pairs added to this PHI node.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_num_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block1_bb = fval.addBasicBlock(name="block1")
///   let block2_bb = fval.addBasicBlock(name="block2")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   let val3 = ctx.getConstInt32(30)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   inspect(phi.getNumIncomingValues(), content="0")
///   phi.addIncoming(val1, entry_bb)
///   inspect(phi.getNumIncomingValues(), content="1")
///   phi.addIncoming(val2, block1_bb)
///   phi.addIncoming(val3, block2_bb)
///   inspect(phi.getNumIncomingValues(), content="3")
/// }
/// ```
pub fn PHINode::getNumIncomingValues(self : PHINode) -> Int {
  self.incomings.length()
}

///|
/// Get the incoming value at the specified index.
///
/// **Note**:
///
/// Returns `None` if the index is out of bounds. Use `PHINode::getNumIncomingValues` to get the valid range.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_value_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block_bb = fval.addBasicBlock(name="block")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block_bb)
///   inspect(phi.getIncomingValue(0).unwrap().getValueRepr(), content="10")
///   inspect(phi.getIncomingValue(1).unwrap().getValueRepr(), content="20")
///   inspect(phi.getIncomingValue(2), content="None")
/// }
/// ```
pub fn PHINode::getIncomingValue(self : PHINode, idx : Int) -> &Value? {
  match self.incomings.get(idx) {
    Some((value, _)) => Some(value)
    None => None
  }
}

///|
/// Get the incoming basic block at the specified index.
///
/// **Note**:
///
/// Returns `None` if the index is out of bounds. Use `PHINode::getNumIncomingValues` to get the valid range.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_block_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block_bb = fval.addBasicBlock(name="block")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block_bb)
///   inspect(phi.getIncomingBlock(0).unwrap().getValueRepr(), content="%entry")
///   inspect(phi.getIncomingBlock(1).unwrap().getValueRepr(), content="%block")
///   inspect(phi.getIncomingBlock(2), content="None")
/// }
/// ```
pub fn PHINode::getIncomingBlock(self : PHINode, idx : Int) -> BasicBlock? {
  match self.incomings.get(idx) {
    Some((_, block)) => Some(block)
    None => None
  }
}

///|
/// Get the incoming value-block pair at the specified index.
///
/// **Note**:
///
/// Returns `None` if the index is out of bounds. This is a convenience method that returns both the value and block together.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_incoming_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block_bb = fval.addBasicBlock(name="block")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block_bb)
///   let (value0, block0) = phi.getIncoming(0).unwrap()
///   inspect(value0.getValueRepr(), content="10")
///   inspect(block0.getValueRepr(), content="%entry")
///   let (value1, block1) = phi.getIncoming(1).unwrap()
///   inspect(value1.getValueRepr(), content="20")
///   inspect(block1.getValueRepr(), content="%block")
///   inspect(phi.getIncoming(2), content="None")
/// }
/// ```
pub fn PHINode::getIncoming(self : PHINode, idx : Int) -> (&Value, BasicBlock)? {
  self.incomings.get(idx)
}

///|
/// Get all incoming value-block pairs as an array.
///
/// **Note**:
///
/// Returns a copy of the internal array containing all value-block pairs in the order they were added.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_incomings_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block1_bb = fval.addBasicBlock(name="block1")
///   let block2_bb = fval.addBasicBlock(name="block2")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   let val3 = ctx.getConstInt32(30)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block1_bb)
///   phi.addIncoming(val3, block2_bb)
///   let incomings = phi.getIncomings()
///   inspect(incomings.length(), content="3")
///   inspect(incomings[0].0.getValueRepr(), content="10")
///   inspect(incomings[0].1.getValueRepr(), content="%entry")
///   inspect(incomings[1].0.getValueRepr(), content="20")
///   inspect(incomings[2].0.getValueRepr(), content="30")
/// }
/// ```
pub fn PHINode::getIncomings(self : PHINode) -> Array[(&Value, BasicBlock)] {
  self.incomings
}

///|
/// Get all incoming values as an array.
///
/// **Note**:
///
/// Returns an array containing only the values from all incoming value-block pairs, in the order they were added.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_values_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block1_bb = fval.addBasicBlock(name="block1")
///   let block2_bb = fval.addBasicBlock(name="block2")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   let val3 = ctx.getConstInt32(30)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block1_bb)
///   phi.addIncoming(val3, block2_bb)
///   let values = phi.getIncomingValues()
///   inspect(values.length(), content="3")
///   inspect(values[0].getValueRepr(), content="10")
///   inspect(values[1].getValueRepr(), content="20")
///   inspect(values[2].getValueRepr(), content="30")
/// }
/// ```
pub fn PHINode::getIncomingValues(self : PHINode) -> Array[&Value] {
  self.incomings.map(incoming => incoming.0)
}

///|
/// Get all incoming basic blocks as an array.
///
/// **Note**:
///
/// Returns an array containing only the basic blocks from all incoming value-block pairs, in the order they were added.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_blocks_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block1_bb = fval.addBasicBlock(name="block1")
///   let block2_bb = fval.addBasicBlock(name="block2")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   let val3 = ctx.getConstInt32(30)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   phi.addIncoming(val1, entry_bb)
///   phi.addIncoming(val2, block1_bb)
///   phi.addIncoming(val3, block2_bb)
///   let blocks = phi.getIncomingBlocks()
///   inspect(blocks.length(), content="3")
///   inspect(blocks[0].getValueRepr(), content="%entry")
///   inspect(blocks[1].getValueRepr(), content="%block1")
///   inspect(blocks[2].getValueRepr(), content="%block2")
/// }
/// ```
pub fn PHINode::getIncomingBlocks(self : PHINode) -> Array[BasicBlock] {
  self.incomings.map(incoming => incoming.1)
}

///|
/// Add an incoming value-block pair to the PHI node.
///
/// **Note**:
///
/// The value's type must match the PHI node's type. Will raise `LLVMValueError` if there is a type mismatch.
/// The basic block represents the predecessor block from which this value flows into the PHI node.
///
/// ```mbt check
/// test {
///   let ctx = Context::new()
///   let mod = ctx.addModule("demo")
///   let builder = ctx.createBuilder()
///   let i32_ty = ctx.getInt32Ty()
///   let fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_add_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   let block1_bb = fval.addBasicBlock(name="block1")
///   let block2_bb = fval.addBasicBlock(name="block2")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   let val1 = ctx.getConstInt32(10)
///   let val2 = ctx.getConstInt32(20)
///   let val3 = ctx.getConstInt32(30)
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty, name="result")
///   inspect(phi.getNumIncomingValues(), content="0")
///   phi.addIncoming(val1, entry_bb)
///   inspect(phi.getNumIncomingValues(), content="1")
///   inspect(phi.getIncomingValue(0).unwrap().getValueRepr(), content="10")
///   inspect(phi.getIncomingBlock(0).unwrap().getValueRepr(), content="%entry")
///   phi.addIncoming(val2, block1_bb)
///   phi.addIncoming(val3, block2_bb)
///   inspect(phi.getNumIncomingValues(), content="3")
///   let expected = "  %result = phi i32 [ 10, %entry ], [ 20, %block1 ], [ 30, %block2 ]"
///   inspect(phi, content=expected)
/// }
/// ```
pub fn PHINode::addIncoming(
  self : PHINode,
  value : &Value,
  block : BasicBlock,
) -> Unit raise LLVMValueError {
  guard value.getType() == self.getType() else {
    let msg = "PHINode incoming value type mismatch: " +
      "expected \{self.getType()}, got \{value.getType()}"
    raise LLVMValueError(msg)
  }
  self.incomings.push((value, block))
  value.addUser(self)
  block.addUser(self)
}

///|
pub impl Value for PHINode with getValueBase(self) {
  ValueBase::{ uid: self.uid, vty: self.vty, 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 fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   builder.setInsertPoint(entry_bb)
///   let phi = builder.createPHI(i32_ty)
///   inspect(phi.getValueRepr(), content="%0")
///   phi.setName("result")
///   inspect(phi.getValueRepr(), content="%result")
/// }
/// ```
pub impl Value for PHINode with getValueRepr(self) {
  match self.getNameOrSlot() {
    Some(Left(name)) => "%\{name}"
    Some(Right(slot)) => "%\{slot}"
    None => ""
  }
}

///|
/// 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 fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_demo")
///   let entry_bb = fval.addBasicBlock(name="entry")
///   builder.setInsertPoint(entry_bb)
///   let phi = builder.createPHI(i32_ty)
///   inspect(phi.getName(), content="None")
///   phi.setName("result")
///   inspect(phi.getName(), content="Some(\"result\")")
/// }
/// ```
pub impl Value for PHINode 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 fty = ctx.getFunctionType(i32_ty, [])
///   let fval = mod.addFunction(fty, "phi_demo")
///   let merge_bb = fval.addBasicBlock(name="merge")
///   builder.setInsertPoint(merge_bb)
///   let phi = builder.createPHI(i32_ty)
///   inspect(phi.getName(), content="None")
///   phi.setName("result")
///   inspect(phi.getName(), content="Some(\"result\")")
/// }
/// ```
pub impl Value for PHINode with setName(self, name) {
  match self.getParent().setSymbol(name, self) {
    EmptyName => {
      let msg = "Misuse `PHINode::setName`: name cannot be empty."
      raise LLVMValueError(msg)
    }
    InvalidName => {
      let msg =
        $|Misuse `PHINode::setName`:
        $|name '\{name}' contains illegal characters,
        $|only alphanumeric characters and underscores are allowed
      raise LLVMValueError(msg)
    }
    DuplicateName(existed) => {
      let msg =
        $|Misuse `PHINode::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 PHINode with removeName(self) {
  match self.name {
    None => ()
    Some(name) => {
      self.getParent().symbols.remove(name)
      self.name = None
    }
  }
}

///|
pub impl Value for PHINode 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 Value for PHINode with asValueEnum(self) {
  PHINode(self)
}

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

///|
pub impl User for PHINode with getUserBase(self) {
  let operands : Array[&Value] = []
  self.incomings.each(incoming => {
    operands.push(incoming.0)
    operands.push(incoming.1)
  })
  UserBase::{ operands, }
}

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

///|
pub impl Instruction for PHINode with asInstEnum(self) {
  InstEnum::PHINode(self)
}

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

///|
pub impl Show for PHINode with output(self, logger) {
  let repr = self.getValueRepr()
  let ty = self.getType()
  let str = "  \{repr} = phi \{ty}"
  let num_incoming = self.getNumIncomingValues()
  let incoming_strs = []
  for i = 0; i < num_incoming; i = i + 1 {
    if self.getIncoming(i) is Some((value, block)) {
      let value_repr = value.getValueRepr()
      let block_repr = block.getValueRepr()
      incoming_strs.push("[ \{value_repr}, \{block_repr} ]")
    }
  }
  let str = str +
    (if num_incoming > 0 { " " + incoming_strs.join(", ") } else { "" })
  logger.write_string(str)
}