use std::collections::{BTreeMap, BTreeSet, 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::{AdapterConfig, AdapterPattern, ResolvedCrateConfig};
use crate::core::ir::{ApiSurface, FunctionDef, TypeDef};
use super::super::assemble_kt_content;
pub(super) fn emit_handle_wrappers(
api: &ApiSurface,
config: &ResolvedCrateConfig,
kotlin_source_dir: &Path,
package: &str,
files: &mut Vec<GeneratedFile>,
bridge_name: &str,
visible_functions: &[&FunctionDef],
) {
let client_types: HashSet<&str> = api
.types
.iter()
.filter(|type_def| has_instance_methods(type_def))
.map(|type_def| type_def.name.as_str())
.collect();
let opaque_type_names: HashSet<&str> = api
.types
.iter()
.filter(|type_def| type_def.is_opaque && !type_def.is_trait)
.map(|type_def| type_def.name.as_str())
.collect();
let exclude_functions: HashSet<String> = config
.kotlin_android
.as_ref()
.map(|android| android.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
let capsule_types = config
.kotlin_android
.as_ref()
.map(|android| android.capsule_types.clone())
.unwrap_or_default();
let handle_type_names = crate::backends::kotlin::handle_only_type_names(
api,
visible_functions,
&exclude_functions,
&opaque_type_names,
&capsule_types,
&client_types,
);
let handle_types: BTreeMap<&str, &TypeDef> = api
.types
.iter()
.filter(|type_def| handle_type_names.contains(type_def.name.as_str()))
.map(|type_def| (type_def.name.as_str(), type_def))
.collect();
for (class_name, type_def) in handle_types {
emit_handle_wrapper(
config,
kotlin_source_dir,
package,
files,
bridge_name,
class_name,
type_def,
);
}
}
fn has_instance_methods(type_def: &TypeDef) -> bool {
type_def.is_opaque
&& !type_def.is_trait
&& type_def
.methods
.iter()
.any(|method| !method.sanitized && !method.is_static)
}
fn emit_handle_wrapper(
config: &ResolvedCrateConfig,
kotlin_source_dir: &Path,
package: &str,
files: &mut Vec<GeneratedFile>,
bridge_name: &str,
class_name: &str,
type_def: &TypeDef,
) {
let mut body = String::new();
let mut imports = BTreeSet::new();
if !type_def.doc.is_empty() {
emit_kdoc_pub(&mut body, &type_def.doc, "");
}
append_handle_header(&mut body, class_name, bridge_name);
let adapters = streaming_adapters(config, class_name);
if !adapters.is_empty() {
add_streaming_imports(&mut imports);
append_streaming_mapper(&mut body);
for adapter in adapters {
append_streaming_method(&mut body, adapter, class_name, bridge_name);
}
}
body.push_str("}\n");
files.push(GeneratedFile {
path: kotlin_source_dir.join(format!("{class_name}.kt")),
content: assemble_kt_content(package, &imports, &body),
generated_header: false,
});
}
fn append_handle_header(body: &mut String, class_name: &str, bridge_name: &str) {
body.push_str(&template_env::render(
"handle_wrapper_header.jinja",
minijinja::context! {
class_name => class_name,
bridge_name => bridge_name,
free_name => format!("nativeFree{}", to_pascal_case(class_name)),
},
));
}
fn streaming_adapters<'a>(config: &'a ResolvedCrateConfig, class_name: &str) -> Vec<&'a AdapterConfig> {
config
.adapters
.iter()
.filter(|adapter| matches!(adapter.pattern, AdapterPattern::Streaming))
.filter(|adapter| {
!adapter
.skip_languages
.iter()
.any(|language| language == "kotlin_android")
})
.filter(|adapter| adapter.owner_type.as_deref() == Some(class_name))
.collect()
}
fn add_streaming_imports(imports: &mut BTreeSet<String>) {
for import in [
"import com.fasterxml.jackson.databind.ObjectMapper",
"import com.fasterxml.jackson.datatype.jdk8.Jdk8Module",
"import com.fasterxml.jackson.databind.PropertyNamingStrategies",
"import kotlinx.coroutines.Dispatchers",
"import kotlinx.coroutines.flow.Flow",
"import kotlinx.coroutines.flow.callbackFlow",
"import kotlinx.coroutines.withContext",
"import kotlinx.coroutines.channels.awaitClose",
] {
imports.insert(import.to_string());
}
}
fn append_streaming_mapper(body: &mut String) {
body.push_str(&template_env::render(
"android_streaming_mapper.jinja",
minijinja::context! {},
));
}
fn append_streaming_method(body: &mut String, adapter: &AdapterConfig, class_name: &str, bridge_name: &str) {
let owner_pascal = to_pascal_case(class_name);
let adapter_pascal = to_pascal_case(&adapter.name);
let first_param_name = adapter
.params
.first()
.map(|param| to_lower_camel(¶m.name))
.unwrap_or_else(|| "request".to_string());
body.push_str(&template_env::render(
"android_streaming_method.jinja",
minijinja::context! {
method_name => to_lower_camel(&adapter.name),
params => streaming_params(adapter),
item_type => adapter.item_type.as_deref().unwrap_or("Any"),
bridge_name => bridge_name,
jni_start => format!("native{owner_pascal}{adapter_pascal}Start"),
jni_next => format!("native{owner_pascal}{adapter_pascal}Next"),
jni_free => format!("native{owner_pascal}{adapter_pascal}Free"),
first_param_name => first_param_name,
},
));
}
fn streaming_params(adapter: &AdapterConfig) -> String {
adapter
.params
.iter()
.map(|param| {
let simple_type = param.ty.rsplit("::").next().unwrap_or(¶m.ty);
format!("{}: {simple_type}", to_lower_camel(¶m.name))
})
.collect::<Vec<_>>()
.join(", ")
}