use crate::core::ir::TypeDef;
use std::collections::HashMap;
use crate::codegen::generators::trait_bridge::format_param_type;
use crate::core::ir::{MethodDef, ReceiverKind};
pub fn gen_options_set_bridge(
prefix: &str,
core_import: &str,
trait_def: &TypeDef,
trait_name: &str,
field_name: &str,
options_type_name: &str,
type_paths: &HashMap<String, String>,
) -> String {
use heck::ToPascalCase;
let pascal_prefix = prefix.to_pascal_case();
let handle_type = format!("{pascal_prefix}{trait_name}Bridge");
let options_type_snake = to_snake_case(options_type_name);
let handle_snake = to_snake_case(&handle_type);
let fn_name = format!("{prefix}_options_set_{field_name}");
let trait_path = trait_def.rust_path.replace('-', "_");
let delegation_methods = gen_vtable_ref_delegation(trait_def, core_import, type_paths);
format!(
r#"/// Attach a vtable visitor bridge to a `{options_type_name}` options struct.
///
/// The `{handle_type}` encapsulates a set of C function pointers that receive visit
/// callbacks during generated conversion. Call this setter before `{prefix}_convert`
/// to activate visitor callbacks. Pass `visitor = null` to clear a previously attached visitor.
///
/// Neither pointer is consumed: the caller retains ownership of both `options` and `visitor`
/// and must free them independently after conversion completes.
///
/// # Safety
///
/// `options` must be a non-null pointer returned by `{prefix}_{options_type_snake}_new` (or
/// equivalent), valid for write access. `visitor` must be a non-null pointer returned by
/// `{prefix}_{handle_snake}_new`, or null. Both must remain valid for the duration of any
/// subsequent `{prefix}_convert` call.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_name}(
options: *mut {core_import}::{options_type_name},
visitor: *mut {handle_type},
) {{
if options.is_null() {{
return;
}}
// SAFETY: null check above guarantees options is a valid, aligned, initialised pointer.
let opts = unsafe {{ &mut *options }};
if visitor.is_null() {{
opts.{field_name} = None;
return;
}}
// Wrap the raw bridge pointer in a thin delegating type that implements the trait.
// `VtableRef` borrows the bridge by raw pointer and must not outlive the bridge handle.
struct VtableRef(*mut {handle_type});
impl std::fmt::Debug for VtableRef {{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
f.debug_tuple("VtableRef").finish()
}}
}}
// SAFETY: {handle_type} is `Send + Sync` (unsafe impl generated by gen_trait_bridge).
// The caller guarantees the pointer remains valid while options is in use.
unsafe impl Send for VtableRef {{}}
// SAFETY: see Send impl above; VtableRef is a transparent wrapper around a raw pointer
// to a type that is itself `Send + Sync`. The outer Arc<Mutex> serialises access.
unsafe impl Sync for VtableRef {{}}
impl {trait_path} for VtableRef {{
{delegation_methods} }}
// SAFETY: visitor is non-null; Arc<Mutex<_>> satisfies VisitorHandle = Arc<Mutex<dyn HtmlVisitor + Send>>.
opts.{field_name} = Some(std::sync::Arc::new(std::sync::Mutex::new(VtableRef(visitor))));
}}"#,
prefix = prefix,
handle_type = handle_type,
handle_snake = handle_snake,
fn_name = fn_name,
core_import = core_import,
options_type_name = options_type_name,
options_type_snake = options_type_snake,
field_name = field_name,
trait_path = trait_path,
delegation_methods = delegation_methods,
)
}
pub fn gen_convert_with_options_field_bridge(prefix: &str, core_import: &str) -> String {
let fn_name = format!("{prefix}_convert");
format!(
r#"/// Run conversion.
///
/// Returns a heap-allocated [`ConversionResult`] on success, or null on failure.
/// Check `{prefix}_last_error_code` / `{prefix}_last_error_context` for error details.
/// The returned pointer must be freed with `{prefix}_conversion_result_free`.
///
/// If a visitor was attached to `options` via `{prefix}_options_set_visitor`, it will
/// receive callbacks during conversion.
///
/// # Arguments
///
/// - `html`: null-terminated, UTF-8 HTML input. Must not be null.
/// - `options`: optional conversion options (with optional embedded visitor); pass null for defaults.
///
/// # Safety
///
/// `html` must be a valid, non-null, null-terminated UTF-8 string.
/// `options` must be a valid pointer or null.
/// Returned pointer must be freed with `{prefix}_conversion_result_free`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn {fn_name}(
html: *const std::ffi::c_char,
options: *const {core_import}::ConversionOptions,
) -> *mut {core_import}::ConversionResult {{
clear_last_error();
if html.is_null() {{
set_last_error(1, "Null pointer passed for html");
return std::ptr::null_mut();
}}
// SAFETY: null check above guarantees html is a valid pointer; string is valid UTF-8 from caller.
let html_str = match unsafe {{ std::ffi::CStr::from_ptr(html) }}.to_str() {{
Ok(s) => s,
Err(_) => {{
set_last_error(1, "Invalid UTF-8 in html parameter");
return std::ptr::null_mut();
}}
}};
// Clone options out of the pointer. Any visitor attached via
// `{prefix}_options_set_visitor` is embedded in options.visitor and will be
// picked up automatically by the core convert call.
let options_rs: Option<{core_import}::ConversionOptions> = if options.is_null() {{
None
}} else {{
// SAFETY: null check above guarantees options is a valid pointer.
Some(unsafe {{ &*options }}.clone())
}};
match {core_import}::convert(html_str, options_rs) {{
Ok(result) => Box::into_raw(Box::new(result)),
Err(e) => {{
set_last_error(2, &e.to_string());
std::ptr::null_mut()
}}
}}
}}"#,
prefix = prefix,
fn_name = fn_name,
core_import = core_import,
)
}
fn gen_vtable_ref_delegation(trait_def: &TypeDef, core_import: &str, type_paths: &HashMap<String, String>) -> String {
let mut out = String::with_capacity(4096);
let own_methods: Vec<&MethodDef> = trait_def.methods.iter().filter(|m| m.trait_source.is_none()).collect();
for method in &own_methods {
let receiver_str = match &method.receiver {
Some(ReceiverKind::Ref) => "&self",
Some(ReceiverKind::RefMut) => "&mut self",
Some(ReceiverKind::Owned) => "self",
None => "",
};
let params: Vec<String> = method
.params
.iter()
.map(|p| format!("{}: {}", p.name, format_param_type(p, type_paths)))
.collect();
let all_params = if receiver_str.is_empty() {
params.join(", ")
} else if params.is_empty() {
receiver_str.to_string()
} else {
format!("{}, {}", receiver_str, params.join(", "))
};
let error_override = method.error_type.as_ref().map(|_| {
"Box<dyn std::error::Error + Send + Sync>".to_string()
});
let ret = crate::codegen::generators::trait_bridge::format_return_type(
&method.return_type,
error_override.as_deref(),
type_paths,
method.returns_ref,
);
let arg_list = build_arg_list(method, core_import, type_paths);
let method_name = &method.name;
out.push_str(&crate::backends::ffi::template_env::render(
"vtable_ref_delegation_method.jinja",
minijinja::context! {
method_name => method_name,
all_params => &all_params,
ret => &ret,
arg_list => &arg_list,
},
));
}
out
}
fn build_arg_list(method: &MethodDef, _core_import: &str, _type_paths: &HashMap<String, String>) -> String {
method
.params
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn to_snake_case(s: &str) -> String {
let mut out = String::new();
for (i, ch) in s.chars().enumerate() {
if ch.is_ascii_uppercase() && i > 0 {
out.push('_');
}
out.push(ch.to_ascii_lowercase());
}
out
}