use crate::core::ir::{MethodDef, TypeDef, TypeRef};
use std::collections::BTreeSet;
use super::types::{escape_kotlin_string, fits_single_line, kotlin_field_default, kotlin_type_with_string_imports};
use crate::backends::kotlin::gen_bindings::helpers::emit_cleaned_kdoc;
use crate::backends::kotlin::gen_bindings::shared::{ValueMethodBridge, kotlin_field_name};
use crate::core::jni::{bridge_method_name, bridgeable_value_methods, is_functional_ref_mut_value_method};
use heck::ToLowerCamelCase;
const VALUE_METHOD_MAPPER: &str = "VALUE_METHOD_MAPPER";
pub(crate) fn emit_type_with_imports(
ty: &TypeDef,
out: &mut String,
imports: &mut BTreeSet<String>,
enum_defaults: &std::collections::HashMap<String, String>,
sealed_class_names: &std::collections::HashSet<String>,
default_constructible_types: &std::collections::HashSet<String>,
value_method_bridge: Option<ValueMethodBridge<'_>>,
) {
emit_cleaned_kdoc(out, &ty.doc, "");
if ty.fields.is_empty() {
out.push_str(&crate::backends::kotlin::template_env::render(
"empty_class.jinja",
minijinja::context! {
name => &ty.name,
},
));
return;
}
let visible_fields: Vec<(usize, &crate::core::ir::FieldDef)> = ty
.fields
.iter()
.enumerate()
.filter(|(_, f)| !f.binding_excluded)
.collect();
let field_sealed_annotations: Vec<Option<String>> = visible_fields
.iter()
.map(|(_, f)| sealed_class_field_annotation(&f.ty, sealed_class_names))
.collect();
let has_field_docs = visible_fields.iter().any(|(_, f)| !f.doc.is_empty());
let has_field_annotations = visible_fields.iter().any(|(_, f)| f.serde_rename.is_some())
|| field_sealed_annotations.iter().any(Option::is_some);
let has_flatten_field = visible_fields.iter().any(|(_, f)| f.serde_flatten);
let mut field_strings: Vec<String> = Vec::with_capacity(visible_fields.len());
for (original_idx, field) in visible_fields.iter() {
let ty_str = kotlin_type_with_string_imports(&field.ty, field.optional, imports);
let name = kotlin_field_name(&field.name, *original_idx);
let (effective_ty_str, default_suffix) = if field.serde_flatten {
let nullable_ty = if ty_str.ends_with('?') {
ty_str.clone()
} else {
format!("{ty_str}?")
};
(nullable_ty, " = null".to_string())
} else {
let default_suffix = kotlin_field_default(
&field.ty,
field.optional,
field.typed_default.as_ref(),
enum_defaults,
default_constructible_types,
);
if default_suffix.contains(".milliseconds") {
imports.insert("import kotlin.time.Duration.Companion.milliseconds".to_string());
}
(ty_str, default_suffix)
};
field_strings.push(format!("val {name}: {effective_ty_str}{default_suffix}"));
}
let bridged_methods: Vec<&MethodDef> = value_method_bridge
.map(|bridge| bridgeable_value_methods(ty, bridge.serde_type_names))
.unwrap_or_default();
let has_instance_methods = !bridged_methods.is_empty();
let prefix = format!("data class {}", ty.name);
let use_single_line = !has_field_docs
&& !has_field_annotations
&& !has_flatten_field
&& !has_instance_methods
&& fits_single_line("", &prefix, &field_strings, "");
if has_flatten_field {
out.push_str("@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown = true)\n");
}
if use_single_line {
out.push_str(&crate::backends::kotlin::template_env::render(
"data_class_inline.jinja",
minijinja::context! {
prefix => prefix,
fields => field_strings.join(", "),
},
));
} else {
out.push_str(&crate::backends::kotlin::template_env::render(
"data_class_header_only.jinja",
minijinja::context! {
prefix => prefix,
},
));
for (idx, ((_, field), field_str)) in visible_fields.iter().zip(field_strings.iter()).enumerate() {
emit_cleaned_kdoc(out, &field.doc, " ");
if let Some(rename) = &field.serde_rename {
out.push_str(&crate::backends::kotlin::template_env::render(
"json_property_annotation.jinja",
minijinja::context! {
indent => " ",
value => escape_kotlin_string(rename),
},
));
}
if let Some(annotation) = &field_sealed_annotations[idx] {
out.push_str(" ");
out.push_str(annotation);
out.push('\n');
}
out.push_str(&crate::backends::kotlin::template_env::render(
"data_class_field_line.jinja",
minijinja::context! {
indent => " ",
field => field_str,
},
));
}
out.push_str(&crate::backends::kotlin::template_env::render(
"data_class_close.jinja",
minijinja::context! {
indent => "",
suffix => if has_instance_methods { " {" } else { "" },
},
));
}
if let Some(bridge) = value_method_bridge {
for &method in &bridged_methods {
emit_value_method(out, ty, method, imports, bridge);
}
}
if has_instance_methods {
out.push_str("}\n");
emit_value_method_mapper(out);
}
}
fn emit_value_method(
out: &mut String,
ty: &TypeDef,
method: &MethodDef,
imports: &mut BTreeSet<String>,
bridge: ValueMethodBridge<'_>,
) {
let method_name = heck::AsLowerCamelCase(method.name.as_str()).to_string();
let returns_receiver = is_functional_ref_mut_value_method(method);
let return_type = if returns_receiver {
TypeRef::Named(ty.name.clone())
} else {
method.return_type.clone()
};
let return_type_str = kotlin_type_with_string_imports(&return_type, false, imports);
let params_sig: Vec<String> = method
.params
.iter()
.map(|p| {
let ptype = kotlin_type_with_string_imports(&p.ty, p.optional, imports);
let pname = p.name.to_lower_camel_case();
format!("{pname}: {ptype}")
})
.collect();
out.push('\n');
emit_cleaned_kdoc(out, &method.doc, " ");
out.push_str(" fun ");
out.push_str(&method_name);
out.push('(');
out.push_str(¶ms_sig.join(", "));
out.push_str("): ");
out.push_str(&return_type_str);
out.push_str(" {\n");
out.push_str(&format!(
" val selfJson = {VALUE_METHOD_MAPPER}.writeValueAsString(this)\n"
));
let mut call_args = vec!["selfJson".to_string()];
if !method.params.is_empty() {
out.push_str(&format!(
" val requestJson = {VALUE_METHOD_MAPPER}.writeValueAsString(\n mapOf(\n"
));
for param in &method.params {
let key = escape_kotlin_string(¶m.name.replace('-', "_"));
let value = param.name.to_lower_camel_case();
out.push_str(&format!(" \"{key}\" to {value},\n"));
}
out.push_str(" ),\n )\n");
call_args.push("requestJson".to_string());
}
let native_call = format!(
"{}.{}({})",
bridge.bridge_class,
bridge_method_name(&ty.name, &method.name),
call_args.join(", ")
);
match &return_type {
TypeRef::Unit => out.push_str(&format!(" {native_call}\n")),
TypeRef::Primitive(_) | TypeRef::String => out.push_str(&format!(" return {native_call}\n")),
_ => {
out.push_str(&format!(" val resultJson = {native_call}\n"));
out.push_str(&format!(" return {VALUE_METHOD_MAPPER}.readValue(\n"));
out.push_str(" resultJson,\n");
out.push_str(&format!(
" object : com.fasterxml.jackson.core.type.TypeReference<{return_type_str}>() {{}},\n"
));
out.push_str(" )\n");
}
}
out.push_str(" }\n");
}
fn emit_value_method_mapper(out: &mut String) {
let declaration = crate::backends::kotlin::template_env::render(
"value_method_mapper.jinja",
minijinja::context! {
name => VALUE_METHOD_MAPPER,
},
);
if !out.contains(&declaration) {
out.push('\n');
out.push_str(&declaration);
}
}
fn sealed_class_field_annotation(
ty: &TypeRef,
sealed_class_names: &std::collections::HashSet<String>,
) -> Option<String> {
let base = match ty {
TypeRef::Optional(inner) => inner.as_ref(),
other => other,
};
match base {
TypeRef::Named(name) if sealed_class_names.contains(name) => Some(format!(
"@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(`as` = {name}::class)"
)),
TypeRef::Vec(inner) => {
let inner_base = match inner.as_ref() {
TypeRef::Optional(i) => i.as_ref(),
other => other,
};
if let TypeRef::Named(name) = inner_base
&& sealed_class_names.contains(name)
{
return Some(format!(
"@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(contentAs = {name}::class)"
));
}
None
}
TypeRef::Map(_, value) => {
let value_base = match value.as_ref() {
TypeRef::Optional(i) => i.as_ref(),
other => other,
};
if let TypeRef::Named(name) = value_base
&& sealed_class_names.contains(name)
{
return Some(format!(
"@field:com.fasterxml.jackson.databind.annotation.JsonSerialize(contentAs = {name}::class)"
));
}
None
}
_ => None,
}
}