use std::collections::{BTreeSet, HashMap, HashSet};
use std::path::Path;
use crate::backends::kotlin::{emit_kdoc_pub, to_lower_camel, to_pascal_case};
use crate::backends::kotlin_android::template_env;
use crate::core::backend::GeneratedFile;
use crate::core::config::{HostCapsuleTypeConfig, ResolvedCrateConfig};
use crate::core::ir::{FunctionDef, TypeRef};
use super::super::assemble_kt_content;
use super::capsule_functions::{emit_capsule_function_wrapper, get_capsule_config};
use super::facade_types::{
bridge_arg, facade_param, facade_return_type, is_dto_named, is_generic_container, render_kotlin_type,
};
use super::helpers::unwrap_optional;
struct FacadeContext<'a> {
config: &'a ResolvedCrateConfig,
bridge_name: &'a str,
opaque_types: &'a HashSet<String>,
async_method_names: HashSet<String>,
capsule_types: HashMap<String, HostCapsuleTypeConfig>,
needs_jackson: bool,
}
struct FunctionProjection {
method_name: String,
params: String,
return_type: String,
bridge_call: String,
call_args: String,
method_already_async: bool,
emit_suspend_wrapper: bool,
}
#[allow(clippy::too_many_arguments)]
pub(super) fn emit_facade(
config: &ResolvedCrateConfig,
kotlin_source_dir: &Path,
package: &str,
files: &mut Vec<GeneratedFile>,
module_name: &str,
bridge_name: &str,
opaque_types: &HashSet<String>,
functions: &[&FunctionDef],
) {
let context = facade_context(config, bridge_name, opaque_types, functions);
let imports = facade_imports(functions, &context);
let mut body = template_env::render(
"module_object_header.jinja",
minijinja::context! { module_name => module_name },
);
if context.needs_jackson {
append_jackson_configuration(&mut body);
}
for function in functions {
emit_function(&mut body, function, &context);
}
body.push_str("}\n");
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{module_name}.kt")),
content: assemble_kt_content(package, &imports, &body),
generated_header: false,
});
}
fn facade_context<'a>(
config: &'a ResolvedCrateConfig,
bridge_name: &'a str,
opaque_types: &'a HashSet<String>,
functions: &[&FunctionDef],
) -> FacadeContext<'a> {
FacadeContext {
config,
bridge_name,
opaque_types,
async_method_names: functions
.iter()
.filter(|function| function.name.ends_with("_async"))
.map(|function| to_lower_camel(&function.name))
.collect(),
capsule_types: config
.kotlin_android
.as_ref()
.map(|android| android.capsule_types.clone())
.unwrap_or_default(),
needs_jackson: functions
.iter()
.any(|function| function_needs_jackson(function, opaque_types)),
}
}
fn function_needs_jackson(function: &FunctionDef, opaque_types: &HashSet<String>) -> bool {
is_dto_named(&function.return_type, opaque_types)
|| is_generic_container(&function.return_type)
|| function.params.iter().any(|param| {
let ty = unwrap_optional(¶m.ty);
is_dto_named(ty, opaque_types) || is_generic_container(ty)
})
}
fn facade_imports(functions: &[&FunctionDef], context: &FacadeContext<'_>) -> BTreeSet<String> {
let mut imports = BTreeSet::new();
if context.needs_jackson {
for import in jackson_imports() {
imports.insert(import.to_string());
}
}
if functions
.iter()
.any(|function| is_generic_container(&function.return_type))
{
imports.insert("import com.fasterxml.jackson.core.type.TypeReference".to_string());
}
imports
}
fn jackson_imports() -> [&'static str; 9] {
[
"import com.fasterxml.jackson.annotation.JsonInclude",
"import com.fasterxml.jackson.databind.DeserializationFeature",
"import com.fasterxml.jackson.databind.PropertyNamingStrategies",
"import com.fasterxml.jackson.datatype.jdk8.Jdk8Module",
"import com.fasterxml.jackson.module.kotlin.KotlinFeature",
"import com.fasterxml.jackson.module.kotlin.KotlinModule",
"import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper",
"import kotlinx.coroutines.Dispatchers",
"import kotlinx.coroutines.withContext",
]
}
fn append_jackson_configuration(body: &mut String) {
body.push_str(&template_env::render(
"android_facade_jackson_config.jinja",
minijinja::context! {},
));
}
fn emit_function(body: &mut String, function: &FunctionDef, context: &FacadeContext<'_>) {
if let Some(capsule) = get_capsule_config(function, &context.capsule_types) {
emit_kdoc_pub(body, &function.doc, " ");
emit_capsule_function_wrapper(body, function, context.bridge_name, capsule);
body.push('\n');
return;
}
emit_kdoc_pub(body, &function.doc, " ");
let projection = project_function(function, context);
if emit_optional_opaque_wrapper(body, function, context.opaque_types, &projection) {
return;
}
if is_dto_named(&function.return_type, context.opaque_types) {
emit_dto_method(body, function, &projection);
} else if is_generic_container(&function.return_type) {
emit_generic_method(body, function, context.opaque_types, &projection);
} else if direct_opaque_type(function, context.opaque_types).is_some() {
emit_opaque_method(body, function, &projection);
} else {
emit_expression_method(body, &projection, projection.bridge_call.clone());
}
}
fn project_function(function: &FunctionDef, context: &FacadeContext<'_>) -> FunctionProjection {
let method_name = to_lower_camel(&function.name);
let params = function
.params
.iter()
.map(|param| facade_param(param, context.opaque_types))
.collect::<Vec<_>>()
.join(", ");
let bridge_args = function
.params
.iter()
.map(|param| bridge_arg(param, context.opaque_types))
.collect::<Vec<_>>()
.join(", ");
let native_name = format!("native{}", to_pascal_case(&function.name));
let method_already_async = method_name.ends_with("Async");
FunctionProjection {
return_type: facade_return_type(&function.return_type, context.opaque_types),
bridge_call: format!("{}.{}({bridge_args})", context.bridge_name, native_name),
call_args: function
.params
.iter()
.map(|param| to_lower_camel(¶m.name))
.collect::<Vec<_>>()
.join(", "),
emit_suspend_wrapper: should_emit_suspend_wrapper(&method_name, method_already_async, context),
method_name,
params,
method_already_async,
}
}
fn should_emit_suspend_wrapper(method_name: &str, method_already_async: bool, context: &FacadeContext<'_>) -> bool {
let generate = context
.config
.generate_overrides
.get("kotlin_android")
.unwrap_or(&context.config.generate);
let wrapper_name = format!("{method_name}Async");
let wrapper_would_collide = !method_already_async && context.async_method_names.contains(&wrapper_name);
method_already_async || (generate.async_wrappers && !wrapper_would_collide)
}
fn emit_optional_opaque_wrapper(
body: &mut String,
function: &FunctionDef,
opaque_types: &HashSet<String>,
projection: &FunctionProjection,
) -> bool {
let TypeRef::Optional(inner) = &function.return_type else {
return false;
};
let TypeRef::Named(type_name) = inner.as_ref() else {
return false;
};
if !opaque_types.contains(type_name) {
return false;
}
let expression = format!("{}.takeIf {{ it != 0L }}?.let(::{type_name})", projection.bridge_call);
body.push_str(&template_env::render(
"android_facade_expr_method.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => format!("{type_name}?"),
expression => expression,
},
));
true
}
fn direct_opaque_type<'a>(function: &'a FunctionDef, opaque_types: &HashSet<String>) -> Option<&'a str> {
match &function.return_type {
TypeRef::Named(type_name) if opaque_types.contains(type_name) => Some(type_name),
_ => None,
}
}
fn emit_dto_method(body: &mut String, function: &FunctionDef, projection: &FunctionProjection) {
let TypeRef::Named(return_class) = &function.return_type else {
unreachable!()
};
if !projection.method_already_async {
body.push_str(&template_env::render(
"android_facade_dto_method.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
bridge_call => projection.bridge_call,
return_class => return_class,
},
));
}
if projection.emit_suspend_wrapper {
emit_kdoc_pub(body, &function.doc, " ");
emit_dto_suspend_method(body, projection, return_class);
}
}
fn emit_dto_suspend_method(body: &mut String, projection: &FunctionProjection, return_class: &str) {
if !projection.method_already_async {
emit_async_delegate(body, projection);
return;
}
body.push_str(&template_env::render(
"android_facade_dto_async_impl.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
bridge_call => projection.bridge_call,
return_class => return_class,
},
));
}
fn emit_generic_method(
body: &mut String,
function: &FunctionDef,
opaque_types: &HashSet<String>,
projection: &FunctionProjection,
) {
let type_reference = render_kotlin_type(&function.return_type, opaque_types);
if !projection.method_already_async {
body.push_str(&template_env::render(
"android_facade_generic_method.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
bridge_call => projection.bridge_call,
type_ref_body => type_reference,
},
));
}
if projection.emit_suspend_wrapper {
emit_kdoc_pub(body, &function.doc, " ");
emit_generic_suspend_method(body, projection, &type_reference);
}
}
fn emit_generic_suspend_method(body: &mut String, projection: &FunctionProjection, type_reference: &str) {
if !projection.method_already_async {
emit_async_delegate(body, projection);
return;
}
body.push_str(&template_env::render(
"android_facade_generic_async_impl.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
bridge_call => projection.bridge_call,
type_reference => type_reference,
},
));
}
fn emit_async_delegate(body: &mut String, projection: &FunctionProjection) {
body.push_str(&template_env::render(
"android_facade_async_method.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
args => projection.call_args,
},
));
}
fn emit_opaque_method(body: &mut String, function: &FunctionDef, projection: &FunctionProjection) {
let TypeRef::Named(type_name) = &function.return_type else {
unreachable!()
};
emit_expression_method(body, projection, format!("{type_name}({})", projection.bridge_call));
}
fn emit_expression_method(body: &mut String, projection: &FunctionProjection, expression: String) {
body.push_str(&template_env::render(
"android_facade_expr_method.jinja",
minijinja::context! {
method_name => projection.method_name,
params => projection.params,
return_type => projection.return_type,
expression => expression,
},
));
}