///|
/// Expressions naming where one call keeps its converted values.
priv struct CallSlots {
  arg : (Int) -> String
  adapter : (Int) -> String
  value : String
  handle : String
}

///|
fn handle_kind(project : Project, name : String) -> String {
  let index = project.handles.search_by(h => h == name).unwrap_or(0)
  // 0x4d4f0001 + index; TokenKind is 0x4d4f0000.
  (1297022977 + index).to_string() + "u"
}

///|
fn find_adapter(
  project : Project,
  pkg_alias : String,
  type_name : String,
) -> AdapterDef raise ConfigError {
  for adapter in project.adapters {
    if adapter.pkg_alias == pkg_alias && adapter.type_name == type_name {
      return adapter
    }
  }
  raise Invalid(
    "generation: platform type @" +
    pkg_alias +
    "." +
    type_name +
    " has no native adapter; add its package with a moonohos.native.json descriptor",
  )
}

///|
/// Converts each argument on the JS thread into owned C++ data or pins.
fn read_arguments(
  call : CallPlan,
  plan : GenerationPlan,
  slots : CallSlots,
) -> String raise ConfigError {
  let mut source = ""
  let mut adapter = 0
  for index, type_index in call.inputs {
    let param = call.function.params[index]
    let path = "\"" + call.function.name + "." + param.name + "\""
    let argv = "argv[" + index.to_string() + "]"
    match param.kind {
      Handle(name) =>
        source += "  if (!ReadHandle(env, " +
          argv +
          ", " +
          handle_kind(plan.project, name) +
          ", " +
          (slots.arg)(index) +
          ", " +
          path +
          ")) return nullptr;\n"
      External(pkg_alias, type_name) => {
        let spec = find_adapter(plan.project, pkg_alias, type_name)
        source += "  " +
          (slots.adapter)(-1) +
          ".push_back(Acquire(env, " +
          argv +
          ", " +
          spec.acquire +
          ", " +
          spec.release +
          ", " +
          path +
          "));\n"
        adapter += 1
      }
      _ =>
        source += "  if (!Read_" +
          type_index.to_string() +
          "(env, " +
          argv +
          ", " +
          (slots.arg)(index) +
          ", input_budget, ancestors, 0, " +
          path +
          ")) return nullptr;\n"
    }
  }
  ignore(adapter)
  source
}

///|
/// Body executed while the gate is held: build MoonBit inputs, call, copy out.
fn locked_body(
  call : CallPlan,
  plan : GenerationPlan,
  slots : CallSlots,
) -> String {
  let types = plan.types
  let mut source = ""
  let args : Array[String] = []
  let mut adapter = 0
  for index, type_index in call.inputs {
    let kind = call.function.params[index].kind
    let suffix = index.to_string()
    match kind {
      Handle(_) => args.push((slots.arg)(index) + ".pin->state->ptr")
      External(_, _) => {
        args.push((slots.adapter)(adapter) + ".ptr")
        adapter += 1
      }
      _ if kind.is_reference() => {
        source += "    MoonOwner<" +
          kind.c_name() +
          "> owned" +
          suffix +
          "{Make_" +
          type_index.to_string() +
          "(" +
          (slots.arg)(index) +
          ")};\n"
        args.push("owned" + suffix + ".ptr")
      }
      _ =>
        args.push(
          "Make_" + type_index.to_string() + "(" + (slots.arg)(index) + ")",
        )
    }
  }
  let mut expression = call.symbol + "(" + args.join(", ") + ")"
  let result = call.function.result
  if call.function.raises {
    let envelope = types[call.raw_output].symbol
    source += "    business_raised = false; business_message.clear();\n    MoonOwner envelope{" +
      expression +
      "};\n    if (" +
      envelope +
      "_length(envelope.ptr) == 0) {\n      if (business_raised) throw BusinessFault(std::move(business_message));\n      throw std::runtime_error(\"empty result envelope\");\n    }\n"
    expression = envelope + "_get(envelope.ptr, 0)"
  }
  let output = "\"" + call.function.name + ".return\""
  match result {
    Scalar(Void) =>
      if !call.function.raises {
        source += "    " + expression + ";\n"
      }
    Handle(_) => source += "    " + slots.handle + " = " + expression + ";\n"
    _ if result.is_reference() =>
      source += "    MoonOwner<" +
        result.c_name() +
        "> returned{" +
        expression +
        "};\n    " +
        slots.value +
        " = Copy_" +
        call.output.to_string() +
        "(returned.ptr, output_budget, 0, " +
        output +
        ");\n"
    _ =>
      source += "    " +
        slots.value +
        " = Copy_" +
        call.output.to_string() +
        "(" +
        expression +
        ", output_budget, 0, " +
        output +
        ");\n"
  }
  source
}

///|
fn bounded_text(call : CallPlan) -> String {
  if call.bounded {
    "true"
  } else {
    "false"
  }
}

///|
fn result_creation(
  call : CallPlan,
  plan : GenerationPlan,
  value : String,
  handle : String,
  state : String,
) -> String {
  match call.function.result {
    Handle(name) =>
      "CreateHandle(env, " +
      state +
      ", std::exchange(" +
      handle +
      ", nullptr), " +
      handle_kind(plan.project, name) +
      ", result)"
    _ => "Create_" + call.output.to_string() + "(env, " + value + ", result)"
  }
}

///|
fn sync_callback(
  call : CallPlan,
  plan : GenerationPlan,
) -> String raise ConfigError {
  let name = call.function.name
  let count = call.inputs.length()
  let slots : CallSlots = {
    arg: i => "arg" + i.to_string(),
    adapter: i => {
      if i < 0 {
        "adapters"
      } else {
        "adapters[" + i.to_string() + "]"
      }
    },
    value: "result_value",
    handle: "handle_result",
  }
  let needs_info = count > 0 || call.function.result is Handle(_)
  let mut source = "static napi_value Call_" +
    name +
    "(napi_env env, napi_callback_info info) {\n try {\n  Budget input_budget{" +
    bounded_text(call) +
    "}, output_budget{" +
    bounded_text(call) +
    "};\n  Ancestors ancestors{env, {}};\n  BridgeValue result_value;\n  void *handle_result = nullptr;\n  std::vector adapters;\n"
  if needs_info {
    source += "  size_t argc = " +
      count.to_string() +
      "; napi_value argv[" +
      count.max(1).to_string() +
      "] = {}; void *data = nullptr;\n  if (!Check(env, napi_get_cb_info(env, info, &argc, " +
      (if count > 0 { "argv" } else { "nullptr" }) +
      ", nullptr, &data))) return nullptr;\n"
    if count > 0 {
      source += "  if (argc < " +
        count.to_string() +
        ") throw TypeFault(\"" +
        name +
        ": missing arguments\");\n"
    }
    source += "  EnvState *state = static_cast(data); (void)state;\n"
  }
  for index in 0.. String raise ConfigError {
  let name = call.function.name
  let count = call.inputs.length()
  let slots : CallSlots = {
    arg: i => "task->args[" + i.to_string() + "]",
    adapter: i => {
      if i < 0 {
        "task->adapters"
      } else {
        "task->adapters[" + i.to_string() + "]"
      }
    },
    value: "task->result_value",
    handle: "task->handle_result",
  }
  let mut source = "static void Execute_" +
    name +
    "(napi_env, void *data) {\n  auto *task = static_cast(data);\n  if (task->cancel->cancelled.load()) { task->outcome = Outcome::Cancelled; return; }\n  try {\n    Budget output_budget{" +
    bounded_text(call) +
    "};\n    { GateLock lock; CancelScope scope(task->cancel.get());\n" +
    locked_body(call, plan, slots) +
    "    if (task->cancel->cancelled.load() && task->handle_result) { moonbit_decref(task->handle_result); task->handle_result = nullptr; }\n    }\n    task->outcome = task->cancel->cancelled.load() ? Outcome::Cancelled : Outcome::Ok;\n  } catch (...) {\n    // A cancelled task reports cancellation even when MoonBit raised (e.g. @task.check()).\n    task->outcome = Classify(std::current_exception(), task->message, task->business);\n    if (task->cancel->cancelled.load()) task->outcome = Outcome::Cancelled;\n  }\n}\n"
  source += "static bool Result_" +
    name +
    "(napi_env env, AsyncTask &task, napi_value &result) {\n  return " +
    result_creation(
      call, plan, "task.result_value", "task.handle_result", "task.state.get()",
    ) +
    ";\n}\nstatic void Complete_" +
    name +
    "(napi_env env, napi_status status, void *data) {\n  SettleTask(env, std::unique_ptr(static_cast(data)), status, Result_" +
    name +
    ");\n}\n"
  source += "static napi_value Call_" +
    name +
    "(napi_env env, napi_callback_info info) {\n try {\n  Budget input_budget{" +
    bounded_text(call) +
    "};\n  Ancestors ancestors{env, {}};\n  size_t argc = " +
    (count + 1).to_string() +
    "; napi_value argv[" +
    (count + 1).to_string() +
    "] = {}; void *data = nullptr;\n  if (!Check(env, napi_get_cb_info(env, info, &argc, argv, nullptr, &data))) return nullptr;\n  if (argc < " +
    count.to_string() +
    ") throw TypeFault(\"" +
    name +
    ": missing arguments\");\n  auto *state = static_cast(data);\n  if (!state) throw std::runtime_error(\"module state unavailable\");\n  std::unique_ptr task(new AsyncTask());\n  task->state = state->shared_from_this();\n  task->args.resize(" +
    count.to_string() +
    ");\n" +
    read_arguments(call, plan, slots) +
    "  if (!ReadToken(env, argc, argv, " +
    count.to_string() +
    ", task->cancel, \"" +
    name +
    ".token\")) return nullptr;\n  return QueueTask(env, std::move(task), \"moonohos." +
    name +
    "\", Execute_" +
    name +
    ", Complete_" +
    name +
    ");\n } catch (...) { ThrowCurrent(env); return nullptr; }\n}\n"
  source
}

///|
fn adapter_declarations(project : Project) -> String {
  let mut source = ""
  let seen : Array[String] = []
  for adapter in project.adapters {
    if !seen.contains(adapter.acquire) {
      seen.push(adapter.acquire)
      source += "extern \"C\" void *" +
        adapter.acquire +
        "(napi_env, napi_value);\nextern \"C\" void " +
        adapter.release +
        "(void *);\n"
    }
  }
  source
}

///|
fn init_source(plan : GenerationPlan) -> String {
  let mut source = "static napi_value Init(napi_env env, napi_value exports) {\n  std::shared_ptr *holder = nullptr;\n  try { holder = new std::shared_ptr(std::make_shared()); }\n  catch (...) { Check(env, napi_throw_error(env, nullptr, \"native allocation failed\")); return nullptr; }\n  if (!Check(env, napi_add_env_cleanup_hook(env, CleanupEnv, holder))) { delete holder; return nullptr; }\n  EnvState *state = holder->get();\n  napi_property_descriptor properties[] = {\n"
  let names = plan.calls.map(c => c.function.name)
  names.push("createCancelToken")
  names.push("moonohos_diagnostics")
  for name in names {
    source += "{\"" +
      name +
      "\", nullptr, Call_" +
      name +
      ", nullptr, nullptr, nullptr, napi_default, state},\n"
  }
  source +
  "  };\n  if (!Check(env, napi_define_properties(env, exports, sizeof(properties) / sizeof(properties[0]), properties))) return nullptr;\n  return exports;\n}\nNAPI_MODULE(" +
  plan.project.native_module +
  ", Init)\n"
}

///|
fn ark_type(project : Project, kind : TypeExpr) -> String raise ConfigError {
  match kind {
    External(pkg_alias, type_name) =>
      find_adapter(project, pkg_alias, type_name).ark_type
    List(element) => "Array<" + ark_type(project, element) + ">"
    _ => kind.ark_name()
  }
}

///|
fn bridge_declarations(project : Project) -> String raise ConfigError {
  let mut source = "// Generated by MoonOHOS " +
    moonohos_version() +
    ". Sync calls throw; async calls return Promises and accept an optional CancelToken.\n"
  let imports : Array[String] = []
  for adapter in project.adapters {
    if !imports.contains(adapter.ark_import) {
      imports.push(adapter.ark_import)
      source += adapter.ark_import + "\n"
    }
  }
  source += record_declarations(project)
  for handle in project.handles {
    source += "export interface " +
      handle +
      " {\n  close(): void;\n  isClosed(): boolean;\n}\n"
  }
  source += "export interface CancelToken {\n  cancel(): void;\n  isCancelled(): boolean;\n}\nexport interface MoonOHOSDiagnostics {\n  liveHandles: number;\n  releasedHandles: number;\n  pendingTasks: number;\n  completedTasks: number;\n  cancelledTasks: number;\n  envCleanups: number;\n}\n"
  for function in project.functions {
    let params = function.params.map(p => {
      p.name + ": " + ark_type(project, p.kind)
    })
    let mut result = ark_type(project, function.result)
    if function.is_async {
      params.push("token?: CancelToken")
      result = "Promise<" + result + ">"
    }
    source += "export const " +
      function.name +
      ": (" +
      params.join(", ") +
      ") => " +
      result +
      ";\n"
  }
  source +
  "export const createCancelToken: () => CancelToken;\nexport const moonohos_diagnostics: () => MoonOHOSDiagnostics;\n"
}

///|
fn bridge_callbacks(plan : GenerationPlan) -> String raise ConfigError {
  let mut source = adapter_declarations(plan.project)
  for call in plan.calls {
    source += if call.function.is_async {
      async_callback(call, plan)
    } else {
      sync_callback(call, plan)
    }
  }
  source
}