///|
fn is_byte_length(raw : String) -> Bool {
  match normalized_type(raw) {
    "int"
    | "signed int"
    | "unsigned int"
    | "long"
    | "unsigned long"
    | "long long"
    | "unsigned long long" => true
    _ => false
  }
}

///|
fn validate_companion(
  owner : CFunction,
  companion : CFunction,
  args : Array[Int],
) -> Unit raise {
  if companion.c_signature.contains("...") ||
    !is_byte_length(companion.canonical_return_type) ||
    companion.parameters.length() != args.length() {
    fail("invalid buffer length or null function: " + companion.name)
  }
  for index, source_index in args {
    if source_index >= owner.parameters.length() ||
      normalized_type(owner.parameters[source_index].canonical_type) !=
      normalized_type(companion.parameters[index].canonical_type) {
      fail("buffer companion argument mismatch: " + companion.name)
    }
  }
}

///|
fn validate_v2_buffers(
  nodes : Array[LocatedNode],
  header : String,
  config : GenerateConfig,
) -> Unit raise {
  if config.schema_version != 2 {
    return
  }
  let main : Map[String, CFunction] = Map([])
  let all : Map[String, CFunction] = Map([])
  for entry in nodes {
    if text_field(entry.node, "kind") == Some("FunctionDecl") {
      match c_function(entry.node, entry.source) {
        Some(function) => {
          all[function.name] = function
          if same_header(entry.source.file, header) {
            main[function.name] = function
          }
        }
        None => ()
      }
    }
  }
  for policy in config.buffers {
    let function = match main.get(policy.function_name) {
      Some(value) if config.allows(policy.function_name) => value
      _ => fail("unknown or filtered buffer function: " + policy.function_name)
    }
    if config.return_buffer_for(function.name) is Some(_) ||
      config.utf8_borrowed_returns.contains(function.name) ||
      config.utf8_owned_returns.contains(function.name) {
      fail("conflicting buffer return policy: " + function.name)
    }
    if policy.pointer >= function.parameters.length() ||
      policy.length >= function.parameters.length() ||
      !is_v2_byte_pointer(
        function.parameters[policy.pointer].canonical_type,
        const_required=policy.direction == "in",
      ) ||
      !is_byte_length(function.parameters[policy.length].canonical_type) {
      fail("buffer pointer or length has unsupported C type: " + function.name)
    }
    if policy.copy_parameter >= 0 {
      if policy.copy_parameter >= function.parameters.length() ||
        !is_v2_callback_pointer(
          function.parameters[policy.copy_parameter].canonical_type,
        ) {
        fail("copy token requires a callback pointer: " + function.name)
      }
    }
    for key, _ in config.output_params {
      if key.has_prefix(function.name + ".") {
        fail("buffer and scalar output policies conflict: " + function.name)
      }
    }
    if function.c_signature.contains("...") ||
      (
        normalized_type(function.canonical_return_type) != "int" &&
        normalized_type(function.canonical_return_type) != "void"
      ) {
      fail("buffer function must return int or void: " + function.name)
    }
  }
  for policy in config.return_buffers {
    let function = match main.get(policy.function_name) {
      Some(value) if config.allows(policy.function_name) => value
      _ => fail("unknown or filtered return buffer: " + policy.function_name)
    }
    if config.buffer_for(function.name) is Some(_) ||
      config.utf8_borrowed_returns.contains(function.name) ||
      config.utf8_owned_returns.contains(function.name) ||
      !is_v2_byte_pointer(function.canonical_return_type, const_required=true) {
      fail(
        "return buffer must be a borrowed const byte pointer: " + function.name,
      )
    }
    let length_function = match all.get(policy.length_function) {
      Some(value) => value
      None => fail("unknown buffer length function: " + policy.length_function)
    }
    validate_companion(function, length_function, policy.length_args)
    if policy.null_function != "" {
      let null_function = match all.get(policy.null_function) {
        Some(value) => value
        None => fail("unknown buffer null function: " + policy.null_function)
      }
      validate_companion(function, null_function, policy.length_args)
    }
  }
}

///|
fn render_v2_buffer(
  function : CFunction,
  aliases : Map[String, String],
  opaque_types : Map[String, Bool],
  enums : Map[String, Bool],
  config : GenerateConfig,
) -> (String?, String?, String) {
  let buffer = config.buffer_for(function.name)
  let returned = config.return_buffer_for(function.name)
  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] = []
  for index, parameter in function.parameters {
    if buffer is Some(policy) && index == policy.pointer {
      if policy.direction == "in" {
        moon_params.push("data : Bytes")
        ffi_params.push("data : Bytes")
        ffi_args.push("data")
        c_params.push("moonbit_bytes_t p" + index.to_string())
        c_args.push("(" + parameter.c_type + ")p" + index.to_string())
        borrowed.push("data")
      } else {
        c_args.push("(" + parameter.c_type + ")mbg_bytes")
      }
    } else if buffer is Some(policy) &&
      index == policy.length &&
      policy.direction == "in" {
      c_args.push("(" + parameter.c_type + ")mbg_len")
    } else if buffer is Some(policy) && index == policy.length {
      let name = "arg" + index.to_string()
      moon_params.push(name + " : Int")
      ffi_params.push(name + " : Int")
      ffi_args.push(name)
      c_params.push("int32_t p" + index.to_string())
      c_args.push("(" + parameter.c_type + ")p" + index.to_string())
    } else if buffer is Some(policy) && index == policy.copy_parameter {
      c_args.push(policy.copy_symbol)
    } else {
      let resolved = 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
        None =>
          return (None, None, "unsupported parameter type: " + parameter.c_type)
      }
      let name = "arg" + index.to_string()
      let declaration = name + " : " + resolved.moon_type
      moon_params.push(declaration)
      ffi_params.push(declaration)
      ffi_args.push(name)
      if resolved.borrowed {
        borrowed.push(name)
        c_params.push("moonbit_bytes_t p" + index.to_string())
        c_args.push("(" + parameter.c_type + ")p" + index.to_string())
      } else {
        c_params.push(parameter.c_type + " p" + index.to_string())
        c_args.push("p" + index.to_string())
      }
    }
  }
  let bridge_name = "mbg_bridge_" + function.moon_name
  let call = function.name + "(" + c_args.join(", ") + ")"
  let public_head = v2_public_head(function.moon_name, moon_params)
  let returns_int = normalized_type(function.canonical_return_type) == "int"
  if returned is Some(policy) {
    let length_args = policy.length_args.map(index => "p" + index.to_string())
    let length_call = policy.length_function +
      "(" +
      length_args.join(", ") +
      ")"
    let null_call = if policy.null_function == "" {
      "0"
    } else {
      policy.null_function +
      "(" +
      length_args.join(", ") +
      ") == " +
      policy.null_symbol
    }
    ffi_params.push("mbg_absent : FixedArray[Int]")
    ffi_args.push("mbg_absent")
    c_params.push("int32_t *mbg_absent")
    borrowed.push("mbg_absent")
    let moon = "///|\n#borrow(" +
      borrowed.join(", ") +
      ")\n" +
      v2_extern(bridge_name, ffi_params, "Bytes") +
      "\n///|\n" +
      public_head +
      " -> Bytes? raise {\n" +
      "  let mbg_absent = FixedArray::make(1, 0)\n" +
      "  let value = " +
      bridge_name +
      "(" +
      ffi_args.join(", ") +
      ")\n" +
      "  if mbg_absent[0] == 1 {\n    None\n  } else if mbg_absent[0] != 0 {\n" +
      "    fail(\"invalid returned buffer length or pointer\")\n" +
      "  } else {\n    Some(value)\n  }\n}\n"
    let c = "MOONBIT_FFI_EXPORT moonbit_bytes_t " +
      bridge_name +
      "(" +
      c_params.join(", ") +
      ") {\n" +
      "  int mbg_is_null = " +
      null_call +
      ";\n" +
      "  const void *mbg_pointer = " +
      call +
      ";\n" +
      "  long long mbg_size = (long long)" +
      length_call +
      ";\n" +
      "  if (mbg_is_null) { mbg_absent[0] = 1; return moonbit_make_bytes(0, 0); }\n" +
      "  if (mbg_size < 0 || mbg_size > INT32_MAX || (mbg_pointer == NULL && mbg_size > 0)) {\n" +
      "    mbg_absent[0] = 2; return moonbit_make_bytes(0, 0);\n" +
      "  }\n  mbg_absent[0] = 0;\n" +
      "  moonbit_bytes_t mbg_bytes = moonbit_make_bytes((int32_t)mbg_size, 0);\n" +
      "  if (mbg_size > 0) memcpy(mbg_bytes, mbg_pointer, (size_t)mbg_size);\n" +
      "  return mbg_bytes;\n}\n"
    return (Some(moon), Some(c), "")
  }
  guard buffer is Some(policy) else {
    return (None, None, "missing buffer policy")
  }
  ffi_params.push("mbg_error : FixedArray[Int]")
  ffi_args.push("mbg_error")
  c_params.push("int32_t *mbg_error")
  borrowed.push("mbg_error")
  if policy.direction == "in" {
    let return_type = if returns_int { "Int" } else { "Unit" }
    let c_return_type = if returns_int { "int32_t" } else { "void" }
    let fail_return = if returns_int { "return 0;" } else { "return;" }
    let call_line = if returns_int {
      "return " + call + ";"
    } else {
      call + ";"
    }
    let moon = "///|\n#borrow(" +
      borrowed.join(", ") +
      ")\n" +
      v2_extern(bridge_name, ffi_params, return_type) +
      "\n///|\n" +
      public_head +
      " -> " +
      return_type +
      " raise {\n" +
      "  let mbg_error = FixedArray::make(1, 0)\n" +
      "  let value = " +
      bridge_name +
      "(" +
      ffi_args.join(", ") +
      ")\n" +
      "  if mbg_error[0] != 0 {\n    fail(\"buffer length exceeds supported range\")\n  }\n" +
      "  value\n}\n"
    let c = "MOONBIT_FFI_EXPORT " +
      c_return_type +
      " " +
      bridge_name +
      "(" +
      c_params.join(", ") +
      ") {\n" +
      "  size_t mbg_len = (size_t)Moonbit_array_length(p" +
      policy.pointer.to_string() +
      ");\n" +
      "  if (mbg_len > INT32_MAX) { mbg_error[0] = 1; " +
      fail_return +
      " }\n  mbg_error[0] = 0;\n  " +
      call_line +
      "\n}\n"
    return (Some(moon), Some(c), "")
  }
  let length_name = "p" + policy.length.to_string()
  let return_type = if returns_int { "(Int, Bytes)" } else { "Bytes" }
  if returns_int {
    ffi_params.push("mbg_status : FixedArray[Int]")
    ffi_args.push("mbg_status")
    c_params.push("int32_t *mbg_status")
    borrowed.push("mbg_status")
  }
  let moon = "///|\n#borrow(" +
    borrowed.join(", ") +
    ")\n" +
    v2_extern(bridge_name, ffi_params, "Bytes") +
    "\n///|\n" +
    public_head +
    " -> " +
    return_type +
    " raise {\n" +
    "  let mbg_error = FixedArray::make(1, 0)\n" +
    (if returns_int {
      "  let mbg_status = FixedArray::make(1, 0)\n"
    } else {
      ""
    }) +
    "  let value = " +
    bridge_name +
    "(" +
    ffi_args.join(", ") +
    ")\n" +
    "  if mbg_error[0] != 0 {\n    fail(\"negative output buffer capacity\")\n  }\n" +
    (if returns_int { "  (mbg_status[0], value)\n" } else { "  value\n" }) +
    "}\n"
  let c = "MOONBIT_FFI_EXPORT moonbit_bytes_t " +
    bridge_name +
    "(" +
    c_params.join(", ") +
    ") {\n" +
    "  if (" +
    length_name +
    " < 0) { mbg_error[0] = 1; " +
    (if returns_int { "mbg_status[0] = 0; " } else { "" }) +
    "return moonbit_make_bytes(0, 0); }\n" +
    "  mbg_error[0] = 0;\n" +
    "  moonbit_bytes_t mbg_bytes = moonbit_make_bytes(" +
    length_name +
    ", 0);\n  " +
    (if returns_int { "mbg_status[0] = " } else { "" }) +
    call +
    ";\n  return mbg_bytes;\n}\n"
  (Some(moon), Some(c), "")
}