///|
fn LibraryLowerer::integer_scalar(
  self : LibraryLowerer,
  expr : ResolvedExpr,
) -> LibraryScalar? raise {
  guard self.expr_type(expr) is @ir.Type::Reference(_, name, []) else {
    return None
  }
  match name.to_canonical() {
    "morphir/SDK:int#int-16" => Some(SignedInt16)
    "morphir/SDK:int#int-32" => Some(SignedInt32)
    "morphir/SDK:int#int-64" => Some(SignedInt64)
    "morphir/SDK:int#byte" => Some(UnsignedByte)
    "morphir/SDK:int#uint-16" => Some(UnsignedInt16)
    "morphir/SDK:int#uint-32" => Some(UnsignedInt32)
    "morphir/SDK:int#uint-64" => Some(UnsignedInt64)
    "morphir/SDK:basics#int" => Some(UnboundedInteger)
    _ => None
  }
}

///|
fn LibraryLowerer::integer_invoke(
  self : LibraryLowerer,
  name : String,
  arguments : Array[(@ir.Value, @ir.Type)],
  result : @ir.Type,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  self.invoke(
    name,
    arguments,
    result,
    source,
    role,
    metadata_key="moonbit-source-number-v1",
    runtime_extension=if name.has_prefix("morphir/moonbit-source:") {
      "morphir-moonbit-source-integer-v1"
    } else {
      ""
    },
  )
}

///|
fn LibraryLowerer::integer_conversion(
  self : LibraryLowerer,
  name : String,
  arguments : Array[(@ir.Value, @ir.Type)],
  result : @ir.Type,
  source : @ir.SourceLocation,
  role : String,
  policy : String,
  input_representation : String,
  output_representation : String,
) -> @ir.Value raise {
  let value = self.integer_invoke(name, arguments, result, source, role)
  let facts = @ir.Data::Object(
    Map::from_array([
      ("kind", @ir.Data::Text("conversion")),
      ("role", @ir.Data::Text(role)),
      ("policy", @ir.Data::Text(policy)),
      ("input_representation", @ir.Data::Text(input_representation)),
      ("output_representation", @ir.Data::Text(output_representation)),
      ("binding", @ir.Data::Text(name)),
      ("source_start_line", @ir.Data::Number(source.start_line.to_string())),
      ("source_start_column", @ir.Data::Number(source.start_column.to_string())),
    ]),
  )
  match value {
    @ir.Value::Apply(attrs, _, _) =>
      attrs.extensions["moonbit-source-number-v1"] = facts
    _ => abort("numeric conversion must apply a binding")
  }
  value
}

///|
fn LibraryLowerer::require_integer_budget(
  self : LibraryLowerer,
  value : @ir.Value,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  let exact = library_integer_type()
  self.integer_invoke(
    "morphir/moonbit-source:integer#require-budget",
    [(value, exact)],
    exact,
    source,
    role,
  )
}

///|
fn LibraryLowerer::require_integer_boundary(
  self : LibraryLowerer,
  value : @ir.Value,
  scalar : LibraryScalar,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  if scalar != OptionalSdkInteger {
    return self.require_integer_budget(value, source, role)
  }
  let item = library_integer_type()
  let optional = LibraryTypeUse::{
    scalar,
    spelling: scalar.source_name(),
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  let callback_type = @ir.Type::Function(
    @ir.TypeAttributes::default(),
    item,
    item,
  )
  let name = self.fresh(value, "optional-integer-item", source)
  let attrs = library_attributes(source, item)
  let callback = @ir.Value::Lambda(
    library_attributes(source, callback_type),
    @ir.Pattern::AsPattern(attrs, @ir.Pattern::WildcardPattern(attrs), name),
    self.require_integer_budget(@ir.Value::Variable(attrs, name), source, role),
  )
  self.invoke(
    "morphir/SDK:maybe#map",
    [(callback, callback_type), (value, optional)],
    optional,
    source,
    "optional-" + role,
  )
}

///|
fn LibraryLowerer::integer_widen(
  self : LibraryLowerer,
  value : @ir.Value,
  scalar : LibraryScalar,
  source : @ir.SourceLocation,
  role : String,
) -> @ir.Value raise {
  if scalar == UnboundedInteger {
    return value
  }
  let bounded = LibraryTypeUse::{
    scalar,
    spelling: scalar.source_name(),
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  let sdk_name = match scalar {
    SignedInt16 => "int-16"
    SignedInt64 => "int-64"
    UnsignedByte => "byte"
    UnsignedInt16 => "uint-16"
    UnsignedInt32 => "uint-32"
    UnsignedInt64 => "uint-64"
    _ => abort("unsupported integer width")
  }
  self.integer_conversion(
    "morphir/SDK:int#from-" + sdk_name,
    [(value, bounded)],
    library_integer_type(),
    source,
    role,
    "exact-widening",
    scalar.source_name(),
    "Morphir.Int",
  )
}

///|
fn LibraryLowerer::integer_normalize(
  self : LibraryLowerer,
  value : @ir.Value,
  scalar : LibraryScalar,
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  if scalar == UnboundedInteger {
    return value
  }
  let bounded = LibraryTypeUse::{
    scalar,
    spelling: scalar.source_name(),
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  self.integer_conversion(
    "morphir/moonbit-source:" + scalar.source_module() + "#normalize",
    [(value, library_integer_type())],
    bounded,
    source,
    "source-width-normalization",
    "source-width-wrap",
    "Morphir.Int",
    scalar.source_name(),
  )
}

///|
fn LibraryLowerer::integer_binary(
  self : LibraryLowerer,
  op : String,
  left : @ir.Value,
  right : @ir.Value,
  scalar : LibraryScalar,
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  let bounded = LibraryTypeUse::{
    scalar,
    spelling: scalar.source_name(),
    source,
    profile: self.profile,
  }.to_ir(self.metadata)
  let exact = library_integer_type()
  let left_name = self.fresh(left, "integer-left", source)
  let right_name = self.fresh(right, "integer-right", source)
  let left_value = @ir.Value::Variable(
    library_attributes(source, bounded),
    left_name,
  )
  let right_value = @ir.Value::Variable(
    library_attributes(source, bounded),
    right_name,
  )
  let operation = match op {
    "+" => "add"
    "-" => "subtract"
    "*" => "multiply"
    "==" => "equal"
    "!=" => "not-equal"
    "<" => "less-than"
    "<=" => "less-than-or-equal"
    ">" => "greater-than"
    ">=" => "greater-than-or-equal"
    _ => abort("checked integer operator")
  }
  let arithmetic = ["+", "-", "*"].contains(op)
  let result_type = if arithmetic { exact } else { self.boolean }
  let calculated = self.integer_invoke(
    "morphir/moonbit-source:integer#" + operation,
    [
      (self.integer_widen(left_value, scalar, source, "widen-left"), exact),
      (self.integer_widen(right_value, scalar, source, "widen-right"), exact),
    ],
    result_type,
    source,
    "integer-" + operation,
  )
  let result = if arithmetic {
    self.integer_normalize(calculated, scalar, source)
  } else {
    calculated
  }
  let output = if arithmetic { bounded } else { self.boolean }
  @ir.Value::LetDefinition(
    library_attributes(source, output),
    left_name,
    @ir.ValueDefinition::ExpressionBody([], bounded, left),
    @ir.Value::LetDefinition(
      library_attributes(source, output),
      right_name,
      @ir.ValueDefinition::ExpressionBody([], bounded, right),
      result,
    ),
  )
}

///|
fn LibraryLowerer::integer_negate(
  self : LibraryLowerer,
  value : @ir.Value,
  scalar : LibraryScalar,
  source : @ir.SourceLocation,
) -> @ir.Value raise {
  let exact = library_integer_type()
  let widened = self.integer_widen(value, scalar, source, "widen-negated")
  let negated = self.integer_invoke(
    "morphir/moonbit-source:integer#negate",
    [(widened, exact)],
    exact,
    source,
    "negate-exact",
  )
  self.integer_normalize(negated, scalar, source)
}