///|
priv struct MultiField {
  index : Int
  name : String
  c_type : String
  moon_type : String
  ffi_c_type : String
}

///|
fn v2_multi_field(
  output : NamedOutput,
  parameter : CParameter,
  aliases : Map[String, String],
  opaque_types : Map[String, Bool],
  enums : Map[String, Bool],
) -> MultiField? {
  let shape = c_type_model(parameter.canonical_type)
  if shape.pointer_depth == 0 ||
    shape.pointee_const ||
    shape.array ||
    shape.function_pointer ||
    parameter.canonical_type.contains(" const ") {
    return None
  }
  let raw = parameter.c_type.trim().to_owned()
  let canonical = parameter.canonical_type.trim().to_owned()
  if !raw.has_suffix("*") || !canonical.has_suffix("*") {
    return None
  }
  let c_type = raw[:raw.length() - 1].trim().to_owned()
  let canonical_value = canonical[:canonical.length() - 1].trim().to_owned()
  let kind = v2_abi_kind(c_type, canonical_value)
  let moon_type = if kind != "" {
    v2_abi_moon_type(kind)
  } else {
    match
      resolve_type(canonical_value, aliases, opaque_types, enums, result=false) {
      Some(value) => value
      None => return None
    }
  }
  let ffi_c_type = if kind != "" { v2_abi_c_type(kind) } else { c_type }
  Some({
    index: output.index,
    name: output.name,
    c_type,
    moon_type,
    ffi_c_type,
  })
}

///|
fn validate_v2_multi_outputs(
  nodes : Array[LocatedNode],
  header : String,
  config : GenerateConfig,
) -> Unit raise {
  if config.schema_version != 2 {
    return
  }
  let main : Map[String, CFunction] = Map([])
  for entry in nodes {
    if text_field(entry.node, "kind") == Some("FunctionDecl") &&
      same_header(entry.source.file, header) {
      match c_function(entry.node, entry.source) {
        Some(function) => main[function.name] = function
        None => ()
      }
    }
  }
  for policy in config.multi_outputs {
    let function = match main.get(policy.function_name) {
      Some(value) if config.allows(policy.function_name) => value
      _ =>
        fail(
          "unknown or filtered multi-output function: " + policy.function_name,
        )
    }
    if config.buffer_for(function.name) is Some(_) ||
      config.return_buffer_for(function.name) is Some(_) ||
      config.utf8_borrowed_returns.contains(function.name) ||
      config.utf8_owned_returns.contains(function.name) {
      fail("conflicting multi-output policy: " + function.name)
    }
    for key, _ in config.output_params {
      if key.has_prefix(function.name + ".") {
        fail(
          "legacy output policy conflicts with multi-output: " + function.name,
        )
      }
    }
    if function.c_signature.contains("...") ||
      (
        normalized_type(function.canonical_return_type) != "int" &&
        normalized_type(function.canonical_return_type) != "void"
      ) {
      fail("multi-output function must return int or void: " + function.name)
    }
    for output in policy.outputs {
      if output.index >= function.parameters.length() {
        fail("multi-output index is out of range: " + function.name)
      }
    }
  }
}

///|
fn render_v2_multi_output(
  function : CFunction,
  aliases : Map[String, String],
  opaque_types : Map[String, Bool],
  enums : Map[String, Bool],
  config : GenerateConfig,
) -> (String?, String?, String) {
  guard config.multi_output_for(function.name) is Some(policy) else {
    return (None, None, "missing multi-output policy")
  }
  let fields : Array[MultiField] = []
  let output_by_index : Map[Int, MultiField] = Map([])
  for output in policy.outputs {
    if output.index >= function.parameters.length() {
      return (None, None, "multi-output index is out of range")
    }
    let field = match
      v2_multi_field(
        output,
        function.parameters[output.index],
        aliases,
        opaque_types,
        enums,
      ) {
      Some(value) => value
      None =>
        return (
          None,
          None,
          "unsupported output type at position " + output.index.to_string(),
        )
    }
    fields.push(field)
    output_by_index[output.index] = field
  }
  let moon_params : Array[String] = []
  let ffi_params : Array[String] = []
  let ffi_args : Array[String] = []
  let c_params : Array[String] = []
  let c_args : Array[String] = []
  let borrowed : Array[String] = []
  let guards : Array[String] = []
  for index, parameter in function.parameters {
    match output_by_index.get(index) {
      Some(_) => c_args.push("&mbg_result->out" + index.to_string())
      None => {
        let kind = v2_abi_kind(parameter.c_type, parameter.canonical_type)
        let moon_type = if kind != "" {
          v2_abi_moon_type(kind)
        } else {
          match
            resolve_configured_type(
              parameter.c_type,
              parameter.canonical_type,
              aliases,
              opaque_types,
              enums,
              config,
              utf8_input=config.is_utf8_input(function.name, parameter.name),
              result=false,
            ) {
            Some(value) => value.moon_type
            None =>
              return (
                None,
                None,
                "unsupported parameter type: " + parameter.c_type,
              )
          }
        }
        let name = "arg" + index.to_string()
        moon_params.push(name + " : " + moon_type)
        ffi_params.push(name + " : " + moon_type)
        ffi_args.push(name)
        let c_name = "p" + index.to_string()
        if kind != "" {
          c_params.push(v2_abi_c_type(kind) + " " + c_name)
          c_args.push("(" + parameter.c_type + ")" + c_name)
          let check = v2_abi_range_check(kind, c_name)
          if check != "" {
            guards.push(check)
          }
        } else if moon_type == "Bytes" {
          borrowed.push(name)
          c_params.push("moonbit_bytes_t " + c_name)
          c_args.push("(" + parameter.c_type + ")" + c_name)
        } else {
          c_params.push(parameter.c_type + " " + c_name)
          c_args.push(c_name)
        }
      }
    }
  }
  let result_name = "MbgResult_" + function.moon_name
  let raw_name = "MbgRaw_" + function.moon_name
  let bridge_name = "mbg_bridge_" + function.moon_name
  let returns_status = normalized_type(function.canonical_return_type) == "int"
  let moon_fields : Array[String] = []
  let constructor_fields : Array[String] = []
  let getters : Array[String] = []
  let c_fields : Array[String] = ["  int32_t error;"]
  let c_getters : Array[String] = []
  if returns_status {
    moon_fields.push("  status : Int")
    constructor_fields.push("    status: " + bridge_name + "_status(raw),")
    c_fields.push("  int32_t status;")
    getters.push(
      "///|\n#borrow(raw)\n" +
      v2_extern(bridge_name + "_status", ["raw : " + raw_name], "Int"),
    )
    c_getters.push(
      "MOONBIT_FFI_EXPORT int32_t " +
      bridge_name +
      "_status(" +
      bridge_name +
      "_result *raw) { return raw->status; }\n",
    )
  }
  for field in fields {
    let suffix = field.index.to_string()
    moon_fields.push("  " + field.name + " : " + field.moon_type)
    constructor_fields.push(
      "    " + field.name + ": " + bridge_name + "_out" + suffix + "(raw),",
    )
    c_fields.push("  " + field.c_type + " out" + suffix + ";")
    getters.push(
      "///|\n#borrow(raw)\n" +
      v2_extern(
        bridge_name + "_out" + suffix,
        ["raw : " + raw_name],
        field.moon_type,
      ),
    )
    c_getters.push(
      "MOONBIT_FFI_EXPORT " +
      field.ffi_c_type +
      " " +
      bridge_name +
      "_out" +
      suffix +
      "(" +
      bridge_name +
      "_result *raw) { return (" +
      field.ffi_c_type +
      ")raw->out" +
      suffix +
      "; }\n",
    )
  }
  let call = function.name + "(" + c_args.join(", ") + ")"
  let moon = "///|\n" +
    "type " +
    raw_name +
    "\n\n" +
    "///|\npub(all) struct " +
    result_name +
    " {\n" +
    moon_fields.join("\n") +
    "\n}\n\n" +
    "///|\n" +
    (if borrowed.length() == 0 {
      ""
    } else {
      "#borrow(" + borrowed.join(", ") + ")\n"
    }) +
    v2_extern(bridge_name, ffi_params, raw_name) +
    "\n" +
    "///|\n#borrow(raw)\n" +
    v2_extern(bridge_name + "_error", ["raw : " + raw_name], "Int") +
    "\n" +
    getters.join("\n") +
    "\n///|\n" +
    v2_public_head(function.moon_name, moon_params) +
    " -> " +
    result_name +
    " raise {\n  let raw = " +
    bridge_name +
    "(" +
    ffi_args.join(", ") +
    ")\n  if " +
    bridge_name +
    "_error(raw) != 0 {\n    fail(\"C scalar argument is out of range\")\n  }\n" +
    "  {\n" +
    constructor_fields.join("\n") +
    "\n  }\n}\n"
  let c = "typedef struct {\n" +
    c_fields.join("\n") +
    "\n} " +
    bridge_name +
    "_result;\n" +
    "static void " +
    bridge_name +
    "_finalize(void *raw) { (void)raw; }\n" +
    "MOONBIT_FFI_EXPORT " +
    bridge_name +
    "_result *" +
    bridge_name +
    "(" +
    c_params.join(", ") +
    ") {\n" +
    "  " +
    bridge_name +
    "_result *mbg_result = (" +
    bridge_name +
    "_result *)moonbit_make_external_object(" +
    bridge_name +
    "_finalize, sizeof(" +
    bridge_name +
    "_result));\n" +
    "  memset(mbg_result, 0, sizeof(*mbg_result));\n" +
    (if guards.length() == 0 {
      ""
    } else {
      "  if (" +
      guards.join(" || ") +
      ") { mbg_result->error = 1; return mbg_result; }\n"
    }) +
    "  " +
    (if returns_status { "mbg_result->status = " } else { "" }) +
    call +
    ";\n  return mbg_result;\n}\n" +
    "MOONBIT_FFI_EXPORT int32_t " +
    bridge_name +
    "_error(" +
    bridge_name +
    "_result *raw) { return raw->error; }\n" +
    c_getters.join("")
  (Some(moon), Some(c), "")
}