///|
fn type_attrs(tpe : @ir.Type) -> @ir.TypeAttributes {
  match tpe {
    Variable(a, _)
    | Reference(a, _, _)
    | Tuple(a, _)
    | Union(a, _)
    | Record(a, _)
    | ExtensibleRecord(a, _, _)
    | Function(a, _, _)
    | Unit(a) => a
  }
}

///|
fn lower_type(
  ctx : Lowering,
  tpe : @ir.Type,
  context : String,
  trail~ : Array[@ir.FQName] = [],
) -> Lowered raise {
  if !type_attrs(tpe).constraints.is_empty() {
    raise Invalid(
      "moonbit.unsupported_constraint", context, "Type constraints require a registered lowering",
    )
  }
  match tpe {
    Variable(_, _) =>
      raise Invalid(
        "moonbit.ambiguous_type", context, "Generic types require specialization before generation",
      )
    Union(_, _) =>
      raise Invalid(
        "moonbit.unsupported_type", context, "Untagged unions are not supported by MoonBit generation",
      )
    ExtensibleRecord(_, _, _) =>
      raise Invalid(
        "moonbit.unsupported_type", context, "Open records require specialization",
      )
    Function(_, a, b) =>
      Arrow(
        lower_type(ctx, a, context, trail~),
        lower_type(ctx, b, context, trail~),
      )
    Tuple(_, members) => {
      if members.length() < 2 {
        raise Invalid(
          "moonbit.unsupported_type", context, "Tuple arity must be at least two",
        )
      }
      Product(members.map(t => lower_type(ctx, t, context, trail~)))
    }
    Record(_, fields) => {
      let names = fields.keys().collect()
      names.sort()
      let lowered = names.map(n => {
        validate_name(n)
        (n, lower_type(ctx, fields[n], context, trail~))
      })
      let key = "T_r_" +
        hex(
          lowered.map(p => p.0.to_canonical() + ":" + lower_key(p.1)).join(";"),
        )
      allocate_fields(ctx.names, key, names)
      ctx.shapes[key] = lowered
      Shape(key, lowered)
    }
    Unit(_) => Atom(Scalar::Unit)
    Reference(_, name, args) => {
      validate_fqname(name)
      match name.to_canonical() {
        "morphir/SDK:list#list" if args.length() == 1 =>
          Sequence(lower_type(ctx, args[0], context, trail~))
        "morphir/SDK:maybe#maybe" if args.length() == 1 =>
          Optional(lower_type(ctx, args[0], context, trail~))
        "morphir/SDK:result#result" if args.length() == 2 =>
          Outcome(
            lower_type(ctx, args[1], context, trail~),
            lower_type(ctx, args[0], context, trail~),
          )
        _ => {
          if !args.is_empty() {
            raise Invalid(
              "moonbit.unsupported_type", context, "Parameterized user types require specialization",
            )
          }
          match ctx.types.get(name) {
            Some(entry) => {
              let owner = name.package_name.to_canonical() +
                ":" +
                name.module_name.to_canonical() +
                "#"
              if !entry.exposed &&
                !context.has_prefix("value:" + owner) &&
                !context.has_prefix("type:" + owner) {
                raise Invalid(
                  "moonbit.inaccessible_type",
                  context,
                  name.to_canonical(),
                )
              }
              match entry.definition {
                TypeAliasDefinition(params, target) => {
                  if !params.is_empty() || trail.contains(name) {
                    raise Invalid(
                      "moonbit.unsupported_type", context, "Generic or cyclic alias",
                    )
                  }
                  lower_type(ctx, target, context, trail=trail + [name])
                }
                CustomTypeDefinition(params, _) => {
                  if !params.is_empty() {
                    raise Invalid(
                      "moonbit.unsupported_type", context, "Generic custom type",
                    )
                  }
                  Nominal(name)
                }
                _ =>
                  raise Invalid(
                    "moonbit.incomplete_type",
                    context,
                    name.to_canonical(),
                  )
              }
            }
            None => {
              if name.to_canonical() == "morphir/moonbit-source:float32#float32" &&
                !ctx.source_extensions.contains("float32") {
                raise Invalid(
                  "moonbit.missing_source_extension",
                  context,
                  name.to_canonical(),
                )
              }
              Atom(scalar_type(tpe, context))
            }
          }
        }
      }
    }
  }
}

///|
fn lower_key(tpe : Lowered) -> String {
  match tpe {
    Atom(s) =>
      match s {
        SignedInt32 => "sdk-int32"
        BoundedInteger(name) => "sdk-" + name
        Integer => "int"
        Float => "float"
        Float32 => "float32"
        Boolean => "bool"
        Text => "text"
        Character => "char"
        Decimal => "decimal"
        Unit => "unit"
      }
    Product(xs) => "tuple(" + xs.map(lower_key).join(",") + ")"
    Shape(n, _) => n
    Arrow(a, b) => "fn(" + lower_key(a) + "," + lower_key(b) + ")"
    Sequence(a) => "list(" + lower_key(a) + ")"
    Optional(a) => "maybe(" + lower_key(a) + ")"
    Outcome(a, e) => "result(" + lower_key(a) + "," + lower_key(e) + ")"
    Nominal(n) => "type(" + n.to_canonical() + ")"
  }
}

///|
fn type_source(ctx : Lowering, tpe : Lowered) -> String {
  match tpe {
    Atom(s) => moon_type(s, ctx.imports)
    Product(xs) => "(" + xs.map(t => type_source(ctx, t)).join(", ") + ")"
    Shape(n, _) => n
    Arrow(a, b) =>
      "((" +
      type_source(ctx, a) +
      ") -> " +
      type_source(ctx, b) +
      " raise " +
      ctx.error_contract.source() +
      ")"
    Sequence(a) => {
      ctx.imports["list"] = true
      "@list.List[" + type_source(ctx, a) + "]"
    }
    Optional(a) => "Option[" + type_source(ctx, a) + "]"
    Outcome(a, e) =>
      "Result[" + type_source(ctx, a) + ", " + type_source(ctx, e) + "]"
    Nominal(n) => ctx.names.names["type:" + n.to_canonical()]
  }
}

///|
fn curry_type(inputs : Array[Lowered], output : Lowered) -> Lowered {
  let mut result = output
  for i = inputs.length() - 1; i >= 0; i = i - 1 {
    result = Arrow(inputs[i], result)
  }
  result
}

///|
fn local_name(name : @ir.Name) -> String raise {
  validate_name(name)
  "x_" + hex(name.to_canonical())
}

///|
fn field_name(ctx : Lowering, shape : String, name : @ir.Name) -> String {
  ctx.names.fields[shape][name]
}

///|
fn curry_source(
  ctx : Lowering,
  inputs : Array[(String, Lowered)],
  output : Lowered,
  body : String,
) -> String {
  let mut source = body
  let mut result = output
  for i = inputs.length() - 1; i >= 0; i = i - 1 {
    source = "fn(" +
      inputs[i].0 +
      " : " +
      type_source(ctx, inputs[i].1) +
      ") -> " +
      type_source(ctx, result) +
      " raise " +
      ctx.error_contract.source() +
      " { " +
      source +
      " }"
    result = Arrow(inputs[i].1, result)
  }
  source
}

///|
/// Marks what a `pub` declaration exposes: record shapes become `pub(all)`, and a type that is not `public` is
/// emitted abstract. MoonBit refuses a `pub` signature that names a `priv` type, but an abstract type is visible to
/// other packages without its constructors.
fn export_type(ctx : Lowering, tpe : Lowered) -> Unit {
  match tpe {
    Nominal(name) =>
      if !ctx.types[name].public {
        ctx.abstract_types[name] = true
      }
    Arrow(a, b) | Outcome(a, b) => {
      export_type(ctx, a)
      export_type(ctx, b)
    }
    Product(xs) =>
      for t in xs {
        export_type(ctx, t)
      }
    Shape(name, fields) => {
      ctx.exported_shapes[name] = true
      for field in fields {
        export_type(ctx, field.1)
      }
    }
    Optional(t) | Sequence(t) => export_type(ctx, t)
    Atom(_) => ()
  }
}