Skip to main content

alef_backend_java/
gen_bindings.rs

1use crate::type_map::{java_boxed_type, java_ffi_type, java_type};
2use ahash::AHashSet;
3use alef_codegen::naming::{to_class_name, to_java_name};
4use alef_core::backend::{Backend, BuildConfig, Capabilities, GeneratedFile};
5use alef_core::config::{AlefConfig, Language, resolve_output_dir};
6use alef_core::hash::{self, CommentStyle};
7use alef_core::ir::{ApiSurface, EnumDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
8use heck::{ToLowerCamelCase, ToPascalCase, ToSnakeCase};
9use std::collections::HashSet;
10use std::fmt::Write;
11use std::path::PathBuf;
12
13/// Names that conflict with methods on `java.lang.Object` and are therefore
14/// illegal as record component names or method names in generated Java code.
15const JAVA_OBJECT_METHOD_NAMES: &[&str] = &[
16    "wait",
17    "notify",
18    "notifyAll",
19    "getClass",
20    "hashCode",
21    "equals",
22    "toString",
23    "clone",
24    "finalize",
25];
26
27/// Returns true if `name` is a tuple/unnamed field index such as `"0"`, `"1"`, `"_0"`, `"_1"`.
28/// Serde represents tuple and newtype variant fields with these numeric names. They are not
29/// real JSON keys and must not be used as Java identifiers.
30/// Escape a string for use inside a Javadoc comment.
31/// Replaces `*/` (which would close the comment) and `@` (which starts a tag).
32fn escape_javadoc_line(s: &str) -> String {
33    let mut result = String::with_capacity(s.len());
34    let mut chars = s.chars().peekable();
35    while let Some(ch) = chars.next() {
36        if ch == '`' {
37            let mut code = String::new();
38            for c in chars.by_ref() {
39                if c == '`' {
40                    break;
41                }
42                code.push(c);
43            }
44            result.push_str("{@code ");
45            result.push_str(&code);
46            result.push('}');
47        } else if ch == '<' {
48            result.push_str("&lt;");
49        } else if ch == '>' {
50            result.push_str("&gt;");
51        } else if ch == '&' {
52            result.push_str("&amp;");
53        } else if ch == '*' && chars.peek() == Some(&'/') {
54            chars.next();
55            result.push_str("* /");
56        } else if ch == '@' {
57            result.push_str("{@literal @}");
58        } else {
59            result.push(ch);
60        }
61    }
62    result
63}
64
65fn is_tuple_field_name(name: &str) -> bool {
66    let stripped = name.trim_start_matches('_');
67    !stripped.is_empty() && stripped.chars().all(|c| c.is_ascii_digit())
68}
69
70/// Sanitise a field/parameter name that would conflict with `java.lang.Object`
71/// methods.  Conflicting names get a `_` suffix (e.g. `wait` -> `wait_`), which
72/// is then converted to camelCase by `to_java_name`.
73fn safe_java_field_name(name: &str) -> String {
74    let java_name = to_java_name(name);
75    if JAVA_OBJECT_METHOD_NAMES.contains(&java_name.as_str()) {
76        format!("{}Value", java_name)
77    } else {
78        java_name
79    }
80}
81
82pub struct JavaBackend;
83
84impl JavaBackend {
85    /// Convert crate name to main class name (PascalCase + "Rs" suffix).
86    ///
87    /// The "Rs" suffix ensures the raw FFI wrapper class has a distinct name from
88    /// the public facade class (which strips the "Rs" suffix). Without this, the
89    /// facade would delegate to itself, causing infinite recursion.
90    fn resolve_main_class(api: &ApiSurface) -> String {
91        let base = to_class_name(&api.crate_name.replace('-', "_"));
92        if base.ends_with("Rs") {
93            base
94        } else {
95            format!("{}Rs", base)
96        }
97    }
98}
99
100impl Backend for JavaBackend {
101    fn name(&self) -> &str {
102        "java"
103    }
104
105    fn language(&self) -> Language {
106        Language::Java
107    }
108
109    fn capabilities(&self) -> Capabilities {
110        Capabilities {
111            supports_async: true,
112            supports_classes: true,
113            supports_enums: true,
114            supports_option: true,
115            supports_result: true,
116            ..Capabilities::default()
117        }
118    }
119
120    fn generate_bindings(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
121        let package = config.java_package();
122        let prefix = config.ffi_prefix();
123        let main_class = Self::resolve_main_class(api);
124        let package_path = package.replace('.', "/");
125
126        let output_dir = resolve_output_dir(
127            config.output.java.as_ref(),
128            &config.crate_config.name,
129            "packages/java/src/main/java/",
130        );
131
132        let base_path = PathBuf::from(&output_dir).join(&package_path);
133
134        // Collect bridge param names and type aliases so we can strip them from generated
135        // function signatures and emit convertWithVisitor instead.
136        let bridge_param_names: HashSet<String> = config
137            .trait_bridges
138            .iter()
139            .filter_map(|b| b.param_name.clone())
140            .collect();
141        let bridge_type_aliases: HashSet<String> = config
142            .trait_bridges
143            .iter()
144            .filter_map(|b| b.type_alias.clone())
145            .collect();
146        let has_visitor_bridge = !config.trait_bridges.is_empty();
147
148        let mut files = Vec::new();
149
150        // 0. package-info.java - required by Checkstyle
151        let description = config
152            .scaffold
153            .as_ref()
154            .and_then(|s| s.description.as_deref())
155            .unwrap_or("High-performance HTML to Markdown converter.");
156        files.push(GeneratedFile {
157            path: base_path.join("package-info.java"),
158            content: format!(
159                "/**\n * {description}\n */\npackage {package};\n",
160                description = description,
161                package = package,
162            ),
163            generated_header: true,
164        });
165
166        // 1. NativeLib.java - FFI method handles
167        files.push(GeneratedFile {
168            path: base_path.join("NativeLib.java"),
169            content: gen_native_lib(api, config, &package, &prefix, has_visitor_bridge),
170            generated_header: true,
171        });
172
173        // 2. Main wrapper class
174        files.push(GeneratedFile {
175            path: base_path.join(format!("{}.java", main_class)),
176            content: gen_main_class(
177                api,
178                config,
179                &package,
180                &main_class,
181                &prefix,
182                &bridge_param_names,
183                &bridge_type_aliases,
184                has_visitor_bridge,
185            ),
186            generated_header: true,
187        });
188
189        // 3. Exception class
190        files.push(GeneratedFile {
191            path: base_path.join(format!("{}Exception.java", main_class)),
192            content: gen_exception_class(&package, &main_class),
193            generated_header: true,
194        });
195
196        // Collect complex enums (enums with data variants and no serde tag) — use Object for these fields.
197        // Tagged unions (serde_tag is set) are now generated as proper sealed interfaces
198        // and can be deserialized as their concrete types, so they are NOT complex_enums.
199        let complex_enums: AHashSet<String> = api
200            .enums
201            .iter()
202            .filter(|e| e.serde_tag.is_none() && e.variants.iter().any(|v| !v.fields.is_empty()))
203            .map(|e| e.name.clone())
204            .collect();
205
206        // Resolve language-level serde rename strategy (always wins over IR type-level).
207        let lang_rename_all = config.serde_rename_all_for_language(Language::Java);
208
209        // 4. Record types
210        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
211            if !typ.is_opaque && !typ.fields.is_empty() {
212                // Skip types that gen_visitor handles with richer visitor-specific versions
213                if has_visitor_bridge && (typ.name == "NodeContext" || typ.name == "VisitResult") {
214                    continue;
215                }
216                files.push(GeneratedFile {
217                    path: base_path.join(format!("{}.java", typ.name)),
218                    content: gen_record_type(&package, typ, &complex_enums, &lang_rename_all),
219                    generated_header: true,
220                });
221                // Generate builder class for types with defaults
222                if typ.has_default {
223                    files.push(GeneratedFile {
224                        path: base_path.join(format!("{}Builder.java", typ.name)),
225                        content: gen_builder_class(&package, typ),
226                        generated_header: true,
227                    });
228                }
229            }
230        }
231
232        // Collect builder class names generated from record types with defaults,
233        // so we can skip opaque types that would collide with them.
234        let builder_class_names: AHashSet<String> = api
235            .types
236            .iter()
237            .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
238            .map(|t| format!("{}Builder", t.name))
239            .collect();
240
241        // 4b. Opaque handle types (skip if a pure-Java builder already covers this name)
242        for typ in api.types.iter().filter(|typ| !typ.is_trait) {
243            if typ.is_opaque && !builder_class_names.contains(&typ.name) {
244                files.push(GeneratedFile {
245                    path: base_path.join(format!("{}.java", typ.name)),
246                    content: gen_opaque_handle_class(&package, typ, &prefix),
247                    generated_header: true,
248                });
249            }
250        }
251
252        // 5. Enums
253        for enum_def in &api.enums {
254            // Skip enums that gen_visitor handles with richer visitor-specific versions
255            if has_visitor_bridge && enum_def.name == "VisitResult" {
256                continue;
257            }
258            files.push(GeneratedFile {
259                path: base_path.join(format!("{}.java", enum_def.name)),
260                content: gen_enum_class(&package, enum_def),
261                generated_header: true,
262            });
263        }
264
265        // 6. Error exception classes
266        for error in &api.errors {
267            for (class_name, content) in alef_codegen::error_gen::gen_java_error_types(error, &package) {
268                files.push(GeneratedFile {
269                    path: base_path.join(format!("{}.java", class_name)),
270                    content,
271                    generated_header: true,
272                });
273            }
274        }
275
276        // 7. Visitor support files (when a trait bridge is configured)
277        if has_visitor_bridge {
278            for (filename, content) in crate::gen_visitor::gen_visitor_files(&package, &main_class) {
279                files.push(GeneratedFile {
280                    path: base_path.join(filename),
281                    content,
282                    generated_header: false, // already has header comment
283                });
284            }
285        }
286
287        // Build adapter body map (consumed by generators via body substitution)
288        let _adapter_bodies = alef_adapters::build_adapter_bodies(config, Language::Java)?;
289
290        Ok(files)
291    }
292
293    fn generate_public_api(&self, api: &ApiSurface, config: &AlefConfig) -> anyhow::Result<Vec<GeneratedFile>> {
294        let package = config.java_package();
295        let prefix = config.ffi_prefix();
296        let main_class = Self::resolve_main_class(api);
297        let package_path = package.replace('.', "/");
298
299        let output_dir = resolve_output_dir(
300            config.output.java.as_ref(),
301            &config.crate_config.name,
302            "packages/java/src/main/java/",
303        );
304
305        let base_path = PathBuf::from(&output_dir).join(&package_path);
306
307        // Collect bridge param names/aliases to strip from the public facade.
308        let bridge_param_names: HashSet<String> = config
309            .trait_bridges
310            .iter()
311            .filter_map(|b| b.param_name.clone())
312            .collect();
313        let bridge_type_aliases: HashSet<String> = config
314            .trait_bridges
315            .iter()
316            .filter_map(|b| b.type_alias.clone())
317            .collect();
318        let has_visitor_bridge = !config.trait_bridges.is_empty();
319
320        // Generate a high-level public API class that wraps the raw FFI class.
321        // Class name = main_class without "Rs" suffix (e.g., HtmlToMarkdownRs -> HtmlToMarkdown)
322        let public_class = main_class.trim_end_matches("Rs").to_string();
323        let facade_content = gen_facade_class(
324            api,
325            &package,
326            &public_class,
327            &main_class,
328            &prefix,
329            &bridge_param_names,
330            &bridge_type_aliases,
331            has_visitor_bridge,
332        );
333
334        Ok(vec![GeneratedFile {
335            path: base_path.join(format!("{}.java", public_class)),
336            content: facade_content,
337            generated_header: true,
338        }])
339    }
340
341    fn build_config(&self) -> Option<BuildConfig> {
342        Some(BuildConfig {
343            tool: "mvn",
344            crate_suffix: "",
345            depends_on_ffi: true,
346            post_build: vec![],
347        })
348    }
349}
350
351// ---------------------------------------------------------------------------
352// NativeLib.java - FFI method handles
353// ---------------------------------------------------------------------------
354
355fn gen_native_lib(
356    api: &ApiSurface,
357    config: &AlefConfig,
358    package: &str,
359    prefix: &str,
360    has_visitor_bridge: bool,
361) -> String {
362    // Generate the class body first, then scan it to determine which imports are needed.
363    let mut body = String::with_capacity(2048);
364    // Derive the native library name from the FFI output path (directory name with hyphens replaced
365    // by underscores), falling back to `{ffi_prefix}_ffi`.
366    let lib_name = config.ffi_lib_name();
367
368    writeln!(body, "final class NativeLib {{").ok();
369    writeln!(body, "    private static final Linker LINKER = Linker.nativeLinker();").ok();
370    writeln!(body, "    private static final SymbolLookup LIB;").ok();
371    writeln!(
372        body,
373        "    private static final String NATIVES_RESOURCE_ROOT = \"/natives\";"
374    )
375    .ok();
376    writeln!(
377        body,
378        "    private static final Object NATIVE_EXTRACT_LOCK = new Object();"
379    )
380    .ok();
381    writeln!(body, "    private static String cachedExtractKey;").ok();
382    writeln!(body, "    private static Path cachedExtractDir;").ok();
383    writeln!(body).ok();
384    writeln!(body, "    static {{").ok();
385    writeln!(body, "        loadNativeLibrary();").ok();
386    writeln!(body, "        LIB = SymbolLookup.loaderLookup();").ok();
387    writeln!(body, "    }}").ok();
388    writeln!(body).ok();
389    writeln!(body, "    private static void loadNativeLibrary() {{").ok();
390    writeln!(
391        body,
392        "        String osName = System.getProperty(\"os.name\", \"\").toLowerCase(java.util.Locale.ROOT);"
393    )
394    .ok();
395    writeln!(
396        body,
397        "        String osArch = System.getProperty(\"os.arch\", \"\").toLowerCase(java.util.Locale.ROOT);"
398    )
399    .ok();
400    writeln!(body).ok();
401    writeln!(body, "        String libName;").ok();
402    writeln!(body, "        String libExt;").ok();
403    writeln!(
404        body,
405        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
406    )
407    .ok();
408    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
409    writeln!(body, "            libExt = \".dylib\";").ok();
410    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
411    writeln!(body, "            libName = \"{}\";", lib_name).ok();
412    writeln!(body, "            libExt = \".dll\";").ok();
413    writeln!(body, "        }} else {{").ok();
414    writeln!(body, "            libName = \"lib{}\";", lib_name).ok();
415    writeln!(body, "            libExt = \".so\";").ok();
416    writeln!(body, "        }}").ok();
417    writeln!(body).ok();
418    writeln!(body, "        String nativesRid = resolveNativesRid(osName, osArch);").ok();
419    writeln!(
420        body,
421        "        String nativesDir = NATIVES_RESOURCE_ROOT + \"/\" + nativesRid;"
422    )
423    .ok();
424    writeln!(body).ok();
425    writeln!(
426        body,
427        "        Path extracted = tryExtractAndLoadFromResources(nativesDir, libName, libExt);"
428    )
429    .ok();
430    writeln!(body, "        if (extracted != null) {{").ok();
431    writeln!(body, "            return;").ok();
432    writeln!(body, "        }}").ok();
433    writeln!(body).ok();
434    writeln!(body, "        try {{").ok();
435    writeln!(body, "            System.loadLibrary(\"{}\");", lib_name).ok();
436    writeln!(body, "        }} catch (UnsatisfiedLinkError e) {{").ok();
437    writeln!(
438        body,
439        "            String msg = \"Failed to load {} native library. Expected resource: \" + nativesDir + \"/\" + libName",
440        lib_name
441    ).ok();
442    writeln!(
443        body,
444        "                    + libExt + \" (RID: \" + nativesRid + \"). \""
445    )
446    .ok();
447    writeln!(
448        body,
449        "                    + \"Ensure the library is bundled in the JAR under natives/{{os-arch}}/, \""
450    )
451    .ok();
452    writeln!(
453        body,
454        "                    + \"or place it on the system library path (java.library.path).\";",
455    )
456    .ok();
457    writeln!(
458        body,
459        "            UnsatisfiedLinkError out = new UnsatisfiedLinkError(msg + \" Original error: \" + e.getMessage());"
460    )
461    .ok();
462    writeln!(body, "            out.initCause(e);").ok();
463    writeln!(body, "            throw out;").ok();
464    writeln!(body, "        }}").ok();
465    writeln!(body, "    }}").ok();
466    writeln!(body).ok();
467    writeln!(
468        body,
469        "    private static Path tryExtractAndLoadFromResources(String nativesDir, String libName, String libExt) {{"
470    )
471    .ok();
472    writeln!(
473        body,
474        "        String resourcePath = nativesDir + \"/\" + libName + libExt;"
475    )
476    .ok();
477    writeln!(
478        body,
479        "        URL resource = NativeLib.class.getResource(resourcePath);"
480    )
481    .ok();
482    writeln!(body, "        if (resource == null) {{").ok();
483    writeln!(body, "            return null;").ok();
484    writeln!(body, "        }}").ok();
485    writeln!(body).ok();
486    writeln!(body, "        try {{").ok();
487    writeln!(
488        body,
489        "            Path tempDir = extractOrReuseNativeDirectory(nativesDir);"
490    )
491    .ok();
492    writeln!(body, "            Path libPath = tempDir.resolve(libName + libExt);").ok();
493    writeln!(body, "            if (!Files.exists(libPath)) {{").ok();
494    writeln!(
495        body,
496        "                throw new UnsatisfiedLinkError(\"Missing extracted native library: \" + libPath);"
497    )
498    .ok();
499    writeln!(body, "            }}").ok();
500    writeln!(body, "            System.load(libPath.toAbsolutePath().toString());").ok();
501    writeln!(body, "            return libPath;").ok();
502    writeln!(body, "        }} catch (Exception e) {{").ok();
503    writeln!(body, "            System.err.println(\"[NativeLib] Failed to extract and load native library from resources: \" + e.getMessage());").ok();
504    writeln!(body, "            return null;").ok();
505    writeln!(body, "        }}").ok();
506    writeln!(body, "    }}").ok();
507    writeln!(body).ok();
508    writeln!(
509        body,
510        "    private static Path extractOrReuseNativeDirectory(String nativesDir) throws Exception {{"
511    )
512    .ok();
513    writeln!(
514        body,
515        "        URL location = NativeLib.class.getProtectionDomain().getCodeSource().getLocation();"
516    )
517    .ok();
518    writeln!(body, "        if (location == null) {{").ok();
519    writeln!(
520        body,
521        "            throw new IllegalStateException(\"Missing code source location for {} JAR\");",
522        lib_name
523    )
524    .ok();
525    writeln!(body, "        }}").ok();
526    writeln!(body).ok();
527    writeln!(body, "        Path codePath = Path.of(location.toURI());").ok();
528    writeln!(
529        body,
530        "        String key = codePath.toAbsolutePath() + \"::\" + nativesDir;"
531    )
532    .ok();
533    writeln!(body).ok();
534    writeln!(body, "        synchronized (NATIVE_EXTRACT_LOCK) {{").ok();
535    writeln!(
536        body,
537        "            if (cachedExtractDir != null && key.equals(cachedExtractKey)) {{"
538    )
539    .ok();
540    writeln!(body, "                return cachedExtractDir;").ok();
541    writeln!(body, "            }}").ok();
542    writeln!(
543        body,
544        "            Path tempDir = Files.createTempDirectory(\"{}_native\");",
545        lib_name
546    )
547    .ok();
548    writeln!(body, "            tempDir.toFile().deleteOnExit();").ok();
549    writeln!(
550        body,
551        "            List<Path> extracted = extractNativeDirectory(codePath, nativesDir, tempDir);"
552    )
553    .ok();
554    writeln!(body, "            if (extracted.isEmpty()) {{").ok();
555    writeln!(body, "                throw new IllegalStateException(\"No native files extracted from resources dir: \" + nativesDir);").ok();
556    writeln!(body, "            }}").ok();
557    writeln!(body, "            cachedExtractKey = key;").ok();
558    writeln!(body, "            cachedExtractDir = tempDir;").ok();
559    writeln!(body, "            return tempDir;").ok();
560    writeln!(body, "        }}").ok();
561    writeln!(body, "    }}").ok();
562    writeln!(body).ok();
563    writeln!(body, "    private static List<Path> extractNativeDirectory(Path codePath, String nativesDir, Path destDir) throws Exception {{").ok();
564    writeln!(
565        body,
566        "        if (!Files.exists(destDir) || !Files.isDirectory(destDir)) {{"
567    )
568    .ok();
569    writeln!(
570        body,
571        "            throw new IllegalArgumentException(\"Destination directory does not exist: \" + destDir);"
572    )
573    .ok();
574    writeln!(body, "        }}").ok();
575    writeln!(body).ok();
576    writeln!(
577        body,
578        "        String prefix = nativesDir.startsWith(\"/\") ? nativesDir.substring(1) : nativesDir;"
579    )
580    .ok();
581    writeln!(body, "        if (!prefix.endsWith(\"/\")) {{").ok();
582    writeln!(body, "            prefix = prefix + \"/\";").ok();
583    writeln!(body, "        }}").ok();
584    writeln!(body).ok();
585    writeln!(body, "        if (Files.isDirectory(codePath)) {{").ok();
586    writeln!(body, "            Path nativesPath = codePath.resolve(prefix);").ok();
587    writeln!(
588        body,
589        "            if (!Files.exists(nativesPath) || !Files.isDirectory(nativesPath)) {{"
590    )
591    .ok();
592    writeln!(body, "                return List.of();").ok();
593    writeln!(body, "            }}").ok();
594    writeln!(body, "            return copyDirectory(nativesPath, destDir);").ok();
595    writeln!(body, "        }}").ok();
596    writeln!(body).ok();
597    writeln!(body, "        List<Path> extracted = new ArrayList<>();").ok();
598    writeln!(body, "        try (JarFile jar = new JarFile(codePath.toFile())) {{").ok();
599    writeln!(body, "            Enumeration<JarEntry> entries = jar.entries();").ok();
600    writeln!(body, "            while (entries.hasMoreElements()) {{").ok();
601    writeln!(body, "                JarEntry entry = entries.nextElement();").ok();
602    writeln!(body, "                String name = entry.getName();").ok();
603    writeln!(
604        body,
605        "                if (!name.startsWith(prefix) || entry.isDirectory()) {{"
606    )
607    .ok();
608    writeln!(body, "                    continue;").ok();
609    writeln!(body, "                }}").ok();
610    writeln!(
611        body,
612        "                String relative = name.substring(prefix.length());"
613    )
614    .ok();
615    writeln!(body, "                Path out = safeResolve(destDir, relative);").ok();
616    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
617    writeln!(body, "                try (var in = jar.getInputStream(entry)) {{").ok();
618    writeln!(
619        body,
620        "                    Files.copy(in, out, StandardCopyOption.REPLACE_EXISTING);"
621    )
622    .ok();
623    writeln!(body, "                }}").ok();
624    writeln!(body, "                out.toFile().deleteOnExit();").ok();
625    writeln!(body, "                extracted.add(out);").ok();
626    writeln!(body, "            }}").ok();
627    writeln!(body, "        }}").ok();
628    writeln!(body, "        return extracted;").ok();
629    writeln!(body, "    }}").ok();
630    writeln!(body).ok();
631    writeln!(
632        body,
633        "    private static List<Path> copyDirectory(Path srcDir, Path destDir) throws Exception {{"
634    )
635    .ok();
636    writeln!(body, "        List<Path> copied = new ArrayList<>();").ok();
637    writeln!(body, "        try (var paths = Files.walk(srcDir)) {{").ok();
638    writeln!(body, "            for (Path src : (Iterable<Path>) paths::iterator) {{").ok();
639    writeln!(body, "                if (Files.isDirectory(src)) {{").ok();
640    writeln!(body, "                    continue;").ok();
641    writeln!(body, "                }}").ok();
642    writeln!(body, "                Path relative = srcDir.relativize(src);").ok();
643    writeln!(
644        body,
645        "                Path out = safeResolve(destDir, relative.toString());"
646    )
647    .ok();
648    writeln!(body, "                Files.createDirectories(out.getParent());").ok();
649    writeln!(
650        body,
651        "                Files.copy(src, out, StandardCopyOption.REPLACE_EXISTING);"
652    )
653    .ok();
654    writeln!(body, "                out.toFile().deleteOnExit();").ok();
655    writeln!(body, "                copied.add(out);").ok();
656    writeln!(body, "            }}").ok();
657    writeln!(body, "        }}").ok();
658    writeln!(body, "        return copied;").ok();
659    writeln!(body, "    }}").ok();
660    writeln!(body).ok();
661    writeln!(
662        body,
663        "    private static Path safeResolve(Path destDir, String relative) throws Exception {{"
664    )
665    .ok();
666    writeln!(
667        body,
668        "        Path normalizedDest = destDir.toAbsolutePath().normalize();"
669    )
670    .ok();
671    writeln!(body, "        Path out = normalizedDest.resolve(relative).normalize();").ok();
672    writeln!(body, "        if (!out.startsWith(normalizedDest)) {{").ok();
673    writeln!(body, "            throw new SecurityException(\"Blocked extracting native file outside destination directory: \" + relative);").ok();
674    writeln!(body, "        }}").ok();
675    writeln!(body, "        return out;").ok();
676    writeln!(body, "    }}").ok();
677    writeln!(body).ok();
678    writeln!(
679        body,
680        "    private static String resolveNativesRid(String osName, String osArch) {{"
681    )
682    .ok();
683    writeln!(body, "        String arch;").ok();
684    writeln!(
685        body,
686        "        if (osArch.contains(\"aarch64\") || osArch.contains(\"arm64\")) {{"
687    )
688    .ok();
689    writeln!(body, "            arch = \"arm64\";").ok();
690    writeln!(
691        body,
692        "        }} else if (osArch.contains(\"x86_64\") || osArch.contains(\"amd64\")) {{"
693    )
694    .ok();
695    writeln!(body, "            arch = \"x86_64\";").ok();
696    writeln!(body, "        }} else {{").ok();
697    writeln!(body, "            arch = osArch.replaceAll(\"[^a-z0-9_]+\", \"\");").ok();
698    writeln!(body, "        }}").ok();
699    writeln!(body).ok();
700    writeln!(body, "        String os;").ok();
701    writeln!(
702        body,
703        "        if (osName.contains(\"mac\") || osName.contains(\"darwin\")) {{"
704    )
705    .ok();
706    writeln!(body, "            os = \"macos\";").ok();
707    writeln!(body, "        }} else if (osName.contains(\"win\")) {{").ok();
708    writeln!(body, "            os = \"windows\";").ok();
709    writeln!(body, "        }} else {{").ok();
710    writeln!(body, "            os = \"linux\";").ok();
711    writeln!(body, "        }}").ok();
712    writeln!(body).ok();
713    writeln!(body, "        return os + \"-\" + arch;").ok();
714    writeln!(body, "    }}").ok();
715    writeln!(body).ok();
716
717    // Generate method handles for free functions.
718    // All functions get handles regardless of is_async — the FFI layer always exposes
719    // synchronous C functions, and the Java async wrapper delegates to the sync method.
720    for func in &api.functions {
721        let ffi_name = format!("{}_{}", prefix, func.name.to_lowercase());
722        let return_layout = gen_ffi_layout(&func.return_type);
723        let param_layouts: Vec<String> = func.params.iter().map(|p| gen_ffi_layout(&p.ty)).collect();
724
725        let layout_str = gen_function_descriptor(&return_layout, &param_layouts);
726
727        let handle_name = format!("{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
728
729        writeln!(
730            body,
731            "    static final MethodHandle {} = LINKER.downcallHandle(",
732            handle_name
733        )
734        .ok();
735        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", ffi_name).ok();
736        writeln!(body, "        {}", layout_str).ok();
737        writeln!(body, "    );").ok();
738    }
739
740    // free_string handle for releasing FFI-allocated strings
741    {
742        let free_name = format!("{}_free_string", prefix);
743        let handle_name = format!("{}_FREE_STRING", prefix.to_uppercase());
744        writeln!(body).ok();
745        writeln!(
746            body,
747            "    static final MethodHandle {} = LINKER.downcallHandle(",
748            handle_name
749        )
750        .ok();
751        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_name).ok();
752        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
753        writeln!(body, "    );").ok();
754    }
755
756    // Error handling — use the FFI's last_error_code and last_error_context symbols
757    {
758        writeln!(
759            body,
760            "    static final MethodHandle {}_LAST_ERROR_CODE = LINKER.downcallHandle(",
761            prefix.to_uppercase()
762        )
763        .ok();
764        writeln!(body, "        LIB.find(\"{}_last_error_code\").orElseThrow(),", prefix).ok();
765        writeln!(body, "        FunctionDescriptor.of(ValueLayout.JAVA_INT)").ok();
766        writeln!(body, "    );").ok();
767
768        writeln!(
769            body,
770            "    static final MethodHandle {}_LAST_ERROR_CONTEXT = LINKER.downcallHandle(",
771            prefix.to_uppercase()
772        )
773        .ok();
774        writeln!(
775            body,
776            "        LIB.find(\"{}_last_error_context\").orElseThrow(),",
777            prefix
778        )
779        .ok();
780        writeln!(body, "        FunctionDescriptor.of(ValueLayout.ADDRESS)").ok();
781        writeln!(body, "    );").ok();
782    }
783
784    // Track emitted free handles to avoid duplicates (a type may appear both as
785    // a function return type AND as an opaque type).
786    let mut emitted_free_handles: AHashSet<String> = AHashSet::new();
787
788    // Build the set of opaque type names so we can pick the right accessor below.
789    let opaque_type_names: AHashSet<String> = api
790        .types
791        .iter()
792        .filter(|t| t.is_opaque)
793        .map(|t| t.name.clone())
794        .collect();
795
796    // Accessor handles for Named return types (struct pointer → field accessor + free)
797    for func in &api.functions {
798        if let TypeRef::Named(name) = &func.return_type {
799            let type_snake = name.to_snake_case();
800            let type_upper = type_snake.to_uppercase();
801            let is_opaque = opaque_type_names.contains(name.as_str());
802
803            if is_opaque {
804                // Opaque handles: the caller wraps the pointer directly, no JSON needed.
805                // No content accessor is emitted for opaque types.
806            } else {
807                // Non-opaque record types: use _to_json to serialize the full struct to JSON,
808                // which the Java side then deserializes with ObjectMapper.
809                // NOTE: _content returns only the markdown string field, not the full JSON.
810                let to_json_handle = format!("{}_{}_TO_JSON", prefix.to_uppercase(), type_upper);
811                let to_json_ffi = format!("{}_{}_to_json", prefix, type_snake);
812                writeln!(body).ok();
813                writeln!(
814                    body,
815                    "    static final MethodHandle {} = LINKER.downcallHandle(",
816                    to_json_handle
817                )
818                .ok();
819                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", to_json_ffi).ok();
820                writeln!(
821                    body,
822                    "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
823                )
824                .ok();
825                writeln!(body, "    );").ok();
826            }
827
828            // _free: (struct_ptr) -> void
829            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
830            let free_ffi = format!("{}_{}_free", prefix, type_snake);
831            if emitted_free_handles.insert(free_handle.clone()) {
832                writeln!(body).ok();
833                writeln!(
834                    body,
835                    "    static final MethodHandle {} = LINKER.downcallHandle(",
836                    free_handle
837                )
838                .ok();
839                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
840                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
841                writeln!(body, "    );").ok();
842            }
843        }
844    }
845
846    // FROM_JSON + FREE handles for non-opaque Named types used as parameters.
847    // These allow serializing a Java record to JSON and passing it to the FFI.
848    let mut emitted_from_json_handles: AHashSet<String> = AHashSet::new();
849    for func in &api.functions {
850        for param in &func.params {
851            // Handle both Named and Optional<Named> params
852            let inner_name = match &param.ty {
853                TypeRef::Named(n) => Some(n.clone()),
854                TypeRef::Optional(inner) => {
855                    if let TypeRef::Named(n) = inner.as_ref() {
856                        Some(n.clone())
857                    } else {
858                        None
859                    }
860                }
861                _ => None,
862            };
863            if let Some(name) = inner_name {
864                if !opaque_type_names.contains(name.as_str()) {
865                    let type_snake = name.to_snake_case();
866                    let type_upper = type_snake.to_uppercase();
867
868                    // _from_json: (char*) -> struct_ptr
869                    let from_json_handle = format!("{}_{}_FROM_JSON", prefix.to_uppercase(), type_upper);
870                    let from_json_ffi = format!("{}_{}_from_json", prefix, type_snake);
871                    if emitted_from_json_handles.insert(from_json_handle.clone()) {
872                        writeln!(body).ok();
873                        writeln!(
874                            body,
875                            "    static final MethodHandle {} = LINKER.downcallHandle(",
876                            from_json_handle
877                        )
878                        .ok();
879                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", from_json_ffi).ok();
880                        writeln!(
881                            body,
882                            "        FunctionDescriptor.of(ValueLayout.ADDRESS, ValueLayout.ADDRESS)"
883                        )
884                        .ok();
885                        writeln!(body, "    );").ok();
886                    }
887
888                    // _free: (struct_ptr) -> void
889                    let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
890                    let free_ffi = format!("{}_{}_free", prefix, type_snake);
891                    if emitted_free_handles.insert(free_handle.clone()) {
892                        writeln!(body).ok();
893                        writeln!(
894                            body,
895                            "    static final MethodHandle {} = LINKER.downcallHandle(",
896                            free_handle
897                        )
898                        .ok();
899                        writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
900                        writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
901                        writeln!(body, "    );").ok();
902                    }
903                }
904            }
905        }
906    }
907
908    // Collect builder class names from record types with defaults, so we skip
909    // opaque types that are superseded by a pure-Java builder class.
910    let builder_class_names: AHashSet<String> = api
911        .types
912        .iter()
913        .filter(|t| !t.is_opaque && !t.fields.is_empty() && t.has_default)
914        .map(|t| format!("{}Builder", t.name))
915        .collect();
916
917    // Free handles for opaque types (handle pointer → void)
918    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
919        if typ.is_opaque && !builder_class_names.contains(&typ.name) {
920            let type_snake = typ.name.to_snake_case();
921            let type_upper = type_snake.to_uppercase();
922            let free_handle = format!("{}_{}_FREE", prefix.to_uppercase(), type_upper);
923            let free_ffi = format!("{}_{}_free", prefix, type_snake);
924            if emitted_free_handles.insert(free_handle.clone()) {
925                writeln!(body).ok();
926                writeln!(
927                    body,
928                    "    static final MethodHandle {} = LINKER.downcallHandle(",
929                    free_handle
930                )
931                .ok();
932                writeln!(body, "        LIB.find(\"{}\").orElseThrow(),", free_ffi).ok();
933                writeln!(body, "        FunctionDescriptor.ofVoid(ValueLayout.ADDRESS)").ok();
934                writeln!(body, "    );").ok();
935            }
936        }
937    }
938
939    // Inject visitor FFI method handles when a trait bridge is configured.
940    if has_visitor_bridge {
941        body.push_str(&crate::gen_visitor::gen_native_lib_visitor_handles(prefix));
942    }
943
944    writeln!(body, "}}").ok();
945
946    // Now assemble the file with only the imports that are actually used in the body.
947    let mut out = String::with_capacity(body.len() + 512);
948
949    out.push_str(&hash::header(CommentStyle::DoubleSlash));
950    writeln!(out, "package {};", package).ok();
951    writeln!(out).ok();
952    if body.contains("Arena") {
953        writeln!(out, "import java.lang.foreign.Arena;").ok();
954    }
955    if body.contains("FunctionDescriptor") {
956        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
957    }
958    if body.contains("Linker") {
959        writeln!(out, "import java.lang.foreign.Linker;").ok();
960    }
961    if body.contains("MemorySegment") {
962        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
963    }
964    if body.contains("SymbolLookup") {
965        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
966    }
967    if body.contains("ValueLayout") {
968        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
969    }
970    if body.contains("MethodHandle") {
971        writeln!(out, "import java.lang.invoke.MethodHandle;").ok();
972    }
973    // Imports required by the JAR-extraction native loader (always present).
974    writeln!(out, "import java.net.URL;").ok();
975    writeln!(out, "import java.nio.file.Files;").ok();
976    writeln!(out, "import java.nio.file.Path;").ok();
977    writeln!(out, "import java.nio.file.StandardCopyOption;").ok();
978    writeln!(out, "import java.util.ArrayList;").ok();
979    writeln!(out, "import java.util.Enumeration;").ok();
980    writeln!(out, "import java.util.List;").ok();
981    writeln!(out, "import java.util.jar.JarEntry;").ok();
982    writeln!(out, "import java.util.jar.JarFile;").ok();
983    writeln!(out).ok();
984
985    out.push_str(&body);
986
987    out
988}
989
990// ---------------------------------------------------------------------------
991// Main wrapper class
992// ---------------------------------------------------------------------------
993
994#[allow(clippy::too_many_arguments)]
995fn gen_main_class(
996    api: &ApiSurface,
997    _config: &AlefConfig,
998    package: &str,
999    class_name: &str,
1000    prefix: &str,
1001    bridge_param_names: &HashSet<String>,
1002    bridge_type_aliases: &HashSet<String>,
1003    has_visitor_bridge: bool,
1004) -> String {
1005    // Build the set of opaque type names so we can distinguish opaque handles from records
1006    let opaque_types: AHashSet<String> = api
1007        .types
1008        .iter()
1009        .filter(|t| t.is_opaque)
1010        .map(|t| t.name.clone())
1011        .collect();
1012
1013    // Generate the class body first, then scan it to determine which imports are needed.
1014    let mut body = String::with_capacity(4096);
1015
1016    writeln!(body, "public final class {} {{", class_name).ok();
1017    writeln!(body, "    private {}() {{ }}", class_name).ok();
1018    writeln!(body).ok();
1019
1020    // Generate static methods for free functions
1021    for func in &api.functions {
1022        // Always generate sync method (bridge params stripped from signature)
1023        gen_sync_function_method(
1024            &mut body,
1025            func,
1026            prefix,
1027            class_name,
1028            &opaque_types,
1029            bridge_param_names,
1030            bridge_type_aliases,
1031        );
1032        writeln!(body).ok();
1033
1034        // Also generate async wrapper if marked as async
1035        if func.is_async {
1036            gen_async_wrapper_method(&mut body, func, bridge_param_names, bridge_type_aliases);
1037            writeln!(body).ok();
1038        }
1039    }
1040
1041    // Inject convertWithVisitor when a visitor bridge is configured.
1042    if has_visitor_bridge {
1043        body.push_str(&crate::gen_visitor::gen_convert_with_visitor_method(class_name, prefix));
1044        writeln!(body).ok();
1045    }
1046
1047    // Add helper methods only if they are referenced in the body
1048    gen_helper_methods(&mut body, prefix, class_name);
1049
1050    writeln!(body, "}}").ok();
1051
1052    // Now assemble the file with only the imports that are actually used in the body.
1053    let mut out = String::with_capacity(body.len() + 512);
1054
1055    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1056    writeln!(out, "package {};", package).ok();
1057    writeln!(out).ok();
1058    if body.contains("Arena") {
1059        writeln!(out, "import java.lang.foreign.Arena;").ok();
1060    }
1061    if body.contains("FunctionDescriptor") {
1062        writeln!(out, "import java.lang.foreign.FunctionDescriptor;").ok();
1063    }
1064    if body.contains("Linker") {
1065        writeln!(out, "import java.lang.foreign.Linker;").ok();
1066    }
1067    if body.contains("MemorySegment") {
1068        writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1069    }
1070    if body.contains("SymbolLookup") {
1071        writeln!(out, "import java.lang.foreign.SymbolLookup;").ok();
1072    }
1073    if body.contains("ValueLayout") {
1074        writeln!(out, "import java.lang.foreign.ValueLayout;").ok();
1075    }
1076    if body.contains("List<") {
1077        writeln!(out, "import java.util.List;").ok();
1078    }
1079    if body.contains("Map<") {
1080        writeln!(out, "import java.util.Map;").ok();
1081    }
1082    if body.contains("Optional<") {
1083        writeln!(out, "import java.util.Optional;").ok();
1084    }
1085    if body.contains("HashMap<") || body.contains("new HashMap") {
1086        writeln!(out, "import java.util.HashMap;").ok();
1087    }
1088    if body.contains("CompletableFuture") {
1089        writeln!(out, "import java.util.concurrent.CompletableFuture;").ok();
1090    }
1091    if body.contains("CompletionException") {
1092        writeln!(out, "import java.util.concurrent.CompletionException;").ok();
1093    }
1094    // Only import the short name `ObjectMapper` when it's used as a type reference (not just via
1095    // `createObjectMapper()` which uses fully qualified names internally).
1096    // Check for " ObjectMapper" (space before) which indicates use as a type, not a method name suffix.
1097    if body.contains(" ObjectMapper") {
1098        writeln!(out, "import com.fasterxml.jackson.databind.ObjectMapper;").ok();
1099    }
1100    writeln!(out).ok();
1101
1102    out.push_str(&body);
1103
1104    out
1105}
1106
1107fn is_bridge_param_java(
1108    param: &alef_core::ir::ParamDef,
1109    bridge_param_names: &HashSet<String>,
1110    bridge_type_aliases: &HashSet<String>,
1111) -> bool {
1112    if bridge_param_names.contains(param.name.as_str()) {
1113        return true;
1114    }
1115    let type_name = match &param.ty {
1116        TypeRef::Named(n) => Some(n.as_str()),
1117        TypeRef::Optional(inner) => {
1118            if let TypeRef::Named(n) = inner.as_ref() {
1119                Some(n.as_str())
1120            } else {
1121                None
1122            }
1123        }
1124        _ => None,
1125    };
1126    type_name.is_some_and(|n| bridge_type_aliases.contains(n))
1127}
1128
1129fn gen_sync_function_method(
1130    out: &mut String,
1131    func: &FunctionDef,
1132    prefix: &str,
1133    class_name: &str,
1134    opaque_types: &AHashSet<String>,
1135    bridge_param_names: &HashSet<String>,
1136    bridge_type_aliases: &HashSet<String>,
1137) {
1138    // Exclude bridge params from the public Java signature.
1139    let params: Vec<String> = func
1140        .params
1141        .iter()
1142        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1143        .map(|p| {
1144            let ptype = java_type(&p.ty);
1145            format!("final {} {}", ptype, to_java_name(&p.name))
1146        })
1147        .collect();
1148
1149    let return_type = java_type(&func.return_type);
1150
1151    writeln!(
1152        out,
1153        "    public static {} {}({}) throws {}Exception {{",
1154        return_type,
1155        to_java_name(&func.name),
1156        params.join(", "),
1157        class_name
1158    )
1159    .ok();
1160
1161    writeln!(out, "        try (var arena = Arena.ofConfined()) {{").ok();
1162
1163    // Collect non-opaque Named params that need FFI pointer cleanup after the call.
1164    // These are Rust-allocated by _from_json and must be freed with _free.
1165    // Bridge params are excluded — they are passed as NULL.
1166    let ffi_ptr_params: Vec<(String, String)> = func
1167        .params
1168        .iter()
1169        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1170        .filter_map(|p| {
1171            let inner_name = match &p.ty {
1172                TypeRef::Named(n) if !opaque_types.contains(n.as_str()) => Some(n.clone()),
1173                TypeRef::Optional(inner) => {
1174                    if let TypeRef::Named(n) = inner.as_ref() {
1175                        if !opaque_types.contains(n.as_str()) {
1176                            Some(n.clone())
1177                        } else {
1178                            None
1179                        }
1180                    } else {
1181                        None
1182                    }
1183                }
1184                _ => None,
1185            };
1186            inner_name.map(|type_name| {
1187                let cname = "c".to_string() + &to_java_name(&p.name);
1188                let type_snake = type_name.to_snake_case();
1189                let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1190                (cname, free_handle)
1191            })
1192        })
1193        .collect();
1194
1195    // Marshal non-bridge parameters (use camelCase Java names)
1196    for param in &func.params {
1197        if is_bridge_param_java(param, bridge_param_names, bridge_type_aliases) {
1198            continue;
1199        }
1200        marshal_param_to_ffi(out, &to_java_name(&param.name), &param.ty, opaque_types, prefix);
1201    }
1202
1203    // Call FFI
1204    let ffi_handle = format!("NativeLib.{}_{}", prefix.to_uppercase(), func.name.to_uppercase());
1205
1206    // Build call args: bridge params get MemorySegment.NULL, others are marshalled normally.
1207    let call_args: Vec<String> = func
1208        .params
1209        .iter()
1210        .map(|p| {
1211            if is_bridge_param_java(p, bridge_param_names, bridge_type_aliases) {
1212                "MemorySegment.NULL".to_string()
1213            } else {
1214                ffi_param_name(&to_java_name(&p.name), &p.ty, opaque_types)
1215            }
1216        })
1217        .collect();
1218
1219    // Emit a helper closure to free FFI-allocated param pointers (e.g. options created by _from_json)
1220    let emit_ffi_ptr_cleanup = |out: &mut String| {
1221        for (cname, free_handle) in &ffi_ptr_params {
1222            writeln!(out, "            if (!{}.equals(MemorySegment.NULL)) {{", cname).ok();
1223            writeln!(out, "                {}.invoke({});", free_handle, cname).ok();
1224            writeln!(out, "            }}").ok();
1225        }
1226    };
1227
1228    if matches!(func.return_type, TypeRef::Unit) {
1229        writeln!(out, "            {}.invoke({});", ffi_handle, call_args.join(", ")).ok();
1230        emit_ffi_ptr_cleanup(out);
1231        writeln!(out, "        }} catch (Throwable e) {{").ok();
1232        writeln!(
1233            out,
1234            "            throw new {}Exception(\"FFI call failed\", e);",
1235            class_name
1236        )
1237        .ok();
1238        writeln!(out, "        }}").ok();
1239    } else if is_ffi_string_return(&func.return_type) {
1240        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1241        writeln!(
1242            out,
1243            "            var resultPtr = (MemorySegment) {}.invoke({});",
1244            ffi_handle,
1245            call_args.join(", ")
1246        )
1247        .ok();
1248        emit_ffi_ptr_cleanup(out);
1249        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1250        writeln!(out, "                checkLastError();").ok();
1251        writeln!(out, "                return null;").ok();
1252        writeln!(out, "            }}").ok();
1253        writeln!(
1254            out,
1255            "            String result = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1256        )
1257        .ok();
1258        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1259        writeln!(out, "            return result;").ok();
1260        writeln!(out, "        }} catch (Throwable e) {{").ok();
1261        writeln!(
1262            out,
1263            "            throw new {}Exception(\"FFI call failed\", e);",
1264            class_name
1265        )
1266        .ok();
1267        writeln!(out, "        }}").ok();
1268    } else if matches!(func.return_type, TypeRef::Named(_)) {
1269        // Named return types: FFI returns a struct pointer.
1270        let return_type_name = match &func.return_type {
1271            TypeRef::Named(name) => name,
1272            _ => unreachable!(),
1273        };
1274        let is_opaque = opaque_types.contains(return_type_name.as_str());
1275
1276        writeln!(
1277            out,
1278            "            var resultPtr = (MemorySegment) {}.invoke({});",
1279            ffi_handle,
1280            call_args.join(", ")
1281        )
1282        .ok();
1283        emit_ffi_ptr_cleanup(out);
1284        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1285        writeln!(out, "                checkLastError();").ok();
1286        writeln!(out, "                return null;").ok();
1287        writeln!(out, "            }}").ok();
1288
1289        if is_opaque {
1290            // Opaque handles: wrap the raw pointer directly, caller owns and will close()
1291            writeln!(out, "            return new {}(resultPtr);", return_type_name).ok();
1292        } else {
1293            // Record types: use _to_json to serialize the full struct to JSON, then deserialize.
1294            // NOTE: _content only returns the markdown string field, not a full JSON object.
1295            let type_snake = return_type_name.to_snake_case();
1296            let free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
1297            let to_json_handle = format!(
1298                "NativeLib.{}_{}_TO_JSON",
1299                prefix.to_uppercase(),
1300                type_snake.to_uppercase()
1301            );
1302            writeln!(
1303                out,
1304                "            var jsonPtr = (MemorySegment) {}.invoke(resultPtr);",
1305                to_json_handle
1306            )
1307            .ok();
1308            writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1309            writeln!(out, "            if (jsonPtr.equals(MemorySegment.NULL)) {{").ok();
1310            writeln!(out, "                checkLastError();").ok();
1311            writeln!(out, "                return null;").ok();
1312            writeln!(out, "            }}").ok();
1313            writeln!(
1314                out,
1315                "            String json = jsonPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1316            )
1317            .ok();
1318            writeln!(
1319                out,
1320                "            NativeLib.{}_FREE_STRING.invoke(jsonPtr);",
1321                prefix.to_uppercase()
1322            )
1323            .ok();
1324            writeln!(
1325                out,
1326                "            return createObjectMapper().readValue(json, {}.class);",
1327                return_type_name
1328            )
1329            .ok();
1330        }
1331
1332        writeln!(out, "        }} catch (Throwable e) {{").ok();
1333        writeln!(
1334            out,
1335            "            throw new {}Exception(\"FFI call failed\", e);",
1336            class_name
1337        )
1338        .ok();
1339        writeln!(out, "        }}").ok();
1340    } else if matches!(func.return_type, TypeRef::Vec(_)) {
1341        // Vec return types: FFI returns a JSON string pointer; deserialize into List<T>.
1342        let free_handle = format!("NativeLib.{}_FREE_STRING", prefix.to_uppercase());
1343        writeln!(
1344            out,
1345            "            var resultPtr = (MemorySegment) {}.invoke({});",
1346            ffi_handle,
1347            call_args.join(", ")
1348        )
1349        .ok();
1350        emit_ffi_ptr_cleanup(out);
1351        writeln!(out, "            if (resultPtr.equals(MemorySegment.NULL)) {{").ok();
1352        writeln!(out, "                return java.util.List.of();").ok();
1353        writeln!(out, "            }}").ok();
1354        writeln!(
1355            out,
1356            "            String json = resultPtr.reinterpret(Long.MAX_VALUE).getString(0);"
1357        )
1358        .ok();
1359        writeln!(out, "            {}.invoke(resultPtr);", free_handle).ok();
1360        // Determine the element type for deserialization
1361        let element_type = match &func.return_type {
1362            TypeRef::Vec(inner) => java_type(inner),
1363            _ => unreachable!(),
1364        };
1365        writeln!(
1366            out,
1367            "            return createObjectMapper().readValue(json, new com.fasterxml.jackson.core.type.TypeReference<java.util.List<{}>>() {{ }});",
1368            element_type
1369        )
1370        .ok();
1371        writeln!(out, "        }} catch (Throwable e) {{").ok();
1372        writeln!(
1373            out,
1374            "            throw new {}Exception(\"FFI call failed\", e);",
1375            class_name
1376        )
1377        .ok();
1378        writeln!(out, "        }}").ok();
1379    } else {
1380        writeln!(
1381            out,
1382            "            var primitiveResult = ({}) {}.invoke({});",
1383            java_ffi_return_cast(&func.return_type),
1384            ffi_handle,
1385            call_args.join(", ")
1386        )
1387        .ok();
1388        emit_ffi_ptr_cleanup(out);
1389        writeln!(out, "            return primitiveResult;").ok();
1390        writeln!(out, "        }} catch (Throwable e) {{").ok();
1391        writeln!(
1392            out,
1393            "            throw new {}Exception(\"FFI call failed\", e);",
1394            class_name
1395        )
1396        .ok();
1397        writeln!(out, "        }}").ok();
1398    }
1399
1400    writeln!(out, "    }}").ok();
1401}
1402
1403fn gen_async_wrapper_method(
1404    out: &mut String,
1405    func: &FunctionDef,
1406    bridge_param_names: &HashSet<String>,
1407    bridge_type_aliases: &HashSet<String>,
1408) {
1409    let params: Vec<String> = func
1410        .params
1411        .iter()
1412        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1413        .map(|p| {
1414            let ptype = java_type(&p.ty);
1415            format!("final {} {}", ptype, to_java_name(&p.name))
1416        })
1417        .collect();
1418
1419    let return_type = match &func.return_type {
1420        TypeRef::Unit => "Void".to_string(),
1421        other => java_boxed_type(other).to_string(),
1422    };
1423
1424    let sync_method_name = to_java_name(&func.name);
1425    let async_method_name = format!("{}Async", sync_method_name);
1426    let param_names: Vec<String> = func
1427        .params
1428        .iter()
1429        .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1430        .map(|p| to_java_name(&p.name))
1431        .collect();
1432
1433    writeln!(
1434        out,
1435        "    public static CompletableFuture<{}> {}({}) {{",
1436        return_type,
1437        async_method_name,
1438        params.join(", ")
1439    )
1440    .ok();
1441    writeln!(out, "        return CompletableFuture.supplyAsync(() -> {{").ok();
1442    writeln!(out, "            try {{").ok();
1443    writeln!(
1444        out,
1445        "                return {}({});",
1446        sync_method_name,
1447        param_names.join(", ")
1448    )
1449    .ok();
1450    writeln!(out, "            }} catch (Throwable e) {{").ok();
1451    writeln!(out, "                throw new CompletionException(e);").ok();
1452    writeln!(out, "            }}").ok();
1453    writeln!(out, "        }});").ok();
1454    writeln!(out, "    }}").ok();
1455}
1456
1457// ---------------------------------------------------------------------------
1458// Exception class
1459// ---------------------------------------------------------------------------
1460
1461fn gen_exception_class(package: &str, class_name: &str) -> String {
1462    let mut out = String::with_capacity(512);
1463
1464    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1465    writeln!(out, "package {};", package).ok();
1466    writeln!(out).ok();
1467
1468    writeln!(out, "/** Exception thrown by {}. */", class_name).ok();
1469    writeln!(out, "public class {}Exception extends Exception {{", class_name).ok();
1470    writeln!(out, "    /** The error code. */").ok();
1471    writeln!(out, "    private final int code;").ok();
1472    writeln!(out).ok();
1473    writeln!(out, "    /** Creates a new {}Exception. */", class_name).ok();
1474    writeln!(
1475        out,
1476        "    public {}Exception(final int code, final String message) {{",
1477        class_name
1478    )
1479    .ok();
1480    writeln!(out, "        super(message);").ok();
1481    writeln!(out, "        this.code = code;").ok();
1482    writeln!(out, "    }}").ok();
1483    writeln!(out).ok();
1484    writeln!(out, "    /** Creates a new {}Exception with a cause. */", class_name).ok();
1485    writeln!(
1486        out,
1487        "    public {}Exception(final String message, final Throwable cause) {{",
1488        class_name
1489    )
1490    .ok();
1491    writeln!(out, "        super(message, cause);").ok();
1492    writeln!(out, "        this.code = -1;").ok();
1493    writeln!(out, "    }}").ok();
1494    writeln!(out).ok();
1495    writeln!(out, "    /** Returns the error code. */").ok();
1496    writeln!(out, "    public int getCode() {{").ok();
1497    writeln!(out, "        return code;").ok();
1498    writeln!(out, "    }}").ok();
1499    writeln!(out, "}}").ok();
1500
1501    out
1502}
1503
1504// ---------------------------------------------------------------------------
1505// High-level facade class (public API)
1506// ---------------------------------------------------------------------------
1507
1508#[allow(clippy::too_many_arguments)]
1509fn gen_facade_class(
1510    api: &ApiSurface,
1511    package: &str,
1512    public_class: &str,
1513    raw_class: &str,
1514    _prefix: &str,
1515    bridge_param_names: &HashSet<String>,
1516    bridge_type_aliases: &HashSet<String>,
1517    has_visitor_bridge: bool,
1518) -> String {
1519    let mut body = String::with_capacity(4096);
1520
1521    writeln!(body, "public final class {} {{", public_class).ok();
1522    writeln!(body, "    private {}() {{ }}", public_class).ok();
1523    writeln!(body).ok();
1524
1525    // Generate static methods for free functions
1526    for func in &api.functions {
1527        // Sync method — bridge params stripped from public signature
1528        let params: Vec<String> = func
1529            .params
1530            .iter()
1531            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1532            .map(|p| {
1533                let ptype = java_type(&p.ty);
1534                format!("final {} {}", ptype, to_java_name(&p.name))
1535            })
1536            .collect();
1537
1538        let return_type = java_type(&func.return_type);
1539
1540        if !func.doc.is_empty() {
1541            writeln!(body, "    /**").ok();
1542            for line in func.doc.lines() {
1543                writeln!(body, "     * {}", line).ok();
1544            }
1545            writeln!(body, "     */").ok();
1546        }
1547
1548        writeln!(
1549            body,
1550            "    public static {} {}({}) throws {}Exception {{",
1551            return_type,
1552            to_java_name(&func.name),
1553            params.join(", "),
1554            raw_class
1555        )
1556        .ok();
1557
1558        // Null checks for required non-bridge parameters
1559        for param in &func.params {
1560            if !param.optional && !is_bridge_param_java(param, bridge_param_names, bridge_type_aliases) {
1561                let pname = to_java_name(&param.name);
1562                writeln!(
1563                    body,
1564                    "        java.util.Objects.requireNonNull({}, \"{} must not be null\");",
1565                    pname, pname
1566                )
1567                .ok();
1568            }
1569        }
1570
1571        // Delegate to raw FFI class — bridge params are stripped from the raw class
1572        // signature, so we must exclude them entirely (not pass null) to match the
1573        // raw class's parameter count.
1574        let call_args: Vec<String> = func
1575            .params
1576            .iter()
1577            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1578            .map(|p| to_java_name(&p.name))
1579            .collect();
1580
1581        if matches!(func.return_type, TypeRef::Unit) {
1582            writeln!(
1583                body,
1584                "        {}.{}({});",
1585                raw_class,
1586                to_java_name(&func.name),
1587                call_args.join(", ")
1588            )
1589            .ok();
1590        } else {
1591            writeln!(
1592                body,
1593                "        return {}.{}({});",
1594                raw_class,
1595                to_java_name(&func.name),
1596                call_args.join(", ")
1597            )
1598            .ok();
1599        }
1600
1601        writeln!(body, "    }}").ok();
1602        writeln!(body).ok();
1603
1604        // Generate overload without optional params (convenience method).
1605        // Only non-bridge params are considered here.
1606        let has_optional = func
1607            .params
1608            .iter()
1609            .any(|p| p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases));
1610        if has_optional {
1611            let required_params: Vec<String> = func
1612                .params
1613                .iter()
1614                .filter(|p| !p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1615                .map(|p| {
1616                    let ptype = java_type(&p.ty);
1617                    format!("final {} {}", ptype, to_java_name(&p.name))
1618                })
1619                .collect();
1620
1621            writeln!(
1622                body,
1623                "    public static {} {}({}) throws {}Exception {{",
1624                return_type,
1625                to_java_name(&func.name),
1626                required_params.join(", "),
1627                raw_class
1628            )
1629            .ok();
1630
1631            // Build call to raw class: bridge params are excluded (stripped from raw
1632            // class signature), optional params passed as null.
1633            let full_args: Vec<String> = func
1634                .params
1635                .iter()
1636                .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1637                .map(|p| {
1638                    if p.optional {
1639                        "null".to_string()
1640                    } else {
1641                        to_java_name(&p.name)
1642                    }
1643                })
1644                .collect();
1645
1646            if matches!(func.return_type, TypeRef::Unit) {
1647                writeln!(
1648                    body,
1649                    "        {}.{}({});",
1650                    raw_class,
1651                    to_java_name(&func.name),
1652                    full_args.join(", ")
1653                )
1654                .ok();
1655            } else {
1656                writeln!(
1657                    body,
1658                    "        return {}.{}({});",
1659                    raw_class,
1660                    to_java_name(&func.name),
1661                    full_args.join(", ")
1662                )
1663                .ok();
1664            }
1665
1666            writeln!(body, "    }}").ok();
1667            writeln!(body).ok();
1668        }
1669    }
1670
1671    // Expose convertWithVisitor in the public facade when visitor bridge is configured.
1672    if has_visitor_bridge {
1673        writeln!(body, "    /**").ok();
1674        writeln!(
1675            body,
1676            "     * Convert HTML to Markdown, invoking visitor callbacks during processing."
1677        )
1678        .ok();
1679        writeln!(body, "     */").ok();
1680        writeln!(
1681            body,
1682            "    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor)"
1683        )
1684        .ok();
1685        writeln!(body, "            throws {}Exception {{", raw_class).ok();
1686        writeln!(
1687            body,
1688            "        return {}.convertWithVisitor(html, options, visitor);",
1689            raw_class
1690        )
1691        .ok();
1692        writeln!(body, "    }}").ok();
1693        writeln!(body).ok();
1694    }
1695
1696    writeln!(body, "}}").ok();
1697
1698    // Now assemble the file with imports
1699    let mut out = String::with_capacity(body.len() + 512);
1700
1701    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1702    writeln!(out, "package {};", package).ok();
1703
1704    // Check what imports are needed based on content
1705    let has_list = body.contains("List<");
1706    let has_map = body.contains("Map<");
1707    let has_optional = body.contains("Optional<");
1708    let has_imports = has_list || has_map || has_optional;
1709
1710    if has_imports {
1711        writeln!(out).ok();
1712        if has_list {
1713            writeln!(out, "import java.util.List;").ok();
1714        }
1715        if has_map {
1716            writeln!(out, "import java.util.Map;").ok();
1717        }
1718        if has_optional {
1719            writeln!(out, "import java.util.Optional;").ok();
1720        }
1721    }
1722
1723    writeln!(out).ok();
1724    out.push_str(&body);
1725
1726    out
1727}
1728
1729// ---------------------------------------------------------------------------
1730// Opaque handle classes
1731// ---------------------------------------------------------------------------
1732
1733fn gen_opaque_handle_class(package: &str, typ: &TypeDef, prefix: &str) -> String {
1734    let mut out = String::with_capacity(1024);
1735    let class_name = &typ.name;
1736    let type_snake = class_name.to_snake_case();
1737
1738    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1739    writeln!(out, "package {};", package).ok();
1740    writeln!(out).ok();
1741    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1742    writeln!(out).ok();
1743
1744    if !typ.doc.is_empty() {
1745        writeln!(out, "/**").ok();
1746        for line in typ.doc.lines() {
1747            writeln!(out, " * {}", line).ok();
1748        }
1749        writeln!(out, " */").ok();
1750    }
1751
1752    writeln!(out, "public class {} implements AutoCloseable {{", class_name).ok();
1753    writeln!(out, "    private final MemorySegment handle;").ok();
1754    writeln!(out).ok();
1755    writeln!(out, "    {}(MemorySegment handle) {{", class_name).ok();
1756    writeln!(out, "        this.handle = handle;").ok();
1757    writeln!(out, "    }}").ok();
1758    writeln!(out).ok();
1759    writeln!(out, "    MemorySegment handle() {{").ok();
1760    writeln!(out, "        return this.handle;").ok();
1761    writeln!(out, "    }}").ok();
1762    writeln!(out).ok();
1763    writeln!(out, "    @Override").ok();
1764    writeln!(out, "    public void close() {{").ok();
1765    writeln!(
1766        out,
1767        "        if (handle != null && !handle.equals(MemorySegment.NULL)) {{"
1768    )
1769    .ok();
1770    writeln!(out, "            try {{").ok();
1771    writeln!(
1772        out,
1773        "                NativeLib.{}_{}_FREE.invoke(handle);",
1774        prefix.to_uppercase(),
1775        type_snake.to_uppercase()
1776    )
1777    .ok();
1778    writeln!(out, "            }} catch (Throwable e) {{").ok();
1779    writeln!(
1780        out,
1781        "                throw new RuntimeException(\"Failed to free {}: \" + e.getMessage(), e);",
1782        class_name
1783    )
1784    .ok();
1785    writeln!(out, "            }}").ok();
1786    writeln!(out, "        }}").ok();
1787    writeln!(out, "    }}").ok();
1788    writeln!(out, "}}").ok();
1789
1790    out
1791}
1792
1793// ---------------------------------------------------------------------------
1794// Record types (Java records)
1795// ---------------------------------------------------------------------------
1796
1797/// Emit a Javadoc comment block into `out` at the given indentation level.
1798///
1799/// `indent` is the leading whitespace prepended to each line (e.g. `""` for
1800/// top-level declarations, `"    "` for class members).  Does nothing when
1801/// `doc` is empty.
1802fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
1803    if doc.is_empty() {
1804        return;
1805    }
1806    writeln!(out, "{indent}/**").ok();
1807    for line in doc.lines() {
1808        if line.is_empty() {
1809            writeln!(out, "{indent} *").ok();
1810        } else {
1811            let escaped = escape_javadoc_line(line);
1812            writeln!(out, "{indent} * {escaped}").ok();
1813        }
1814    }
1815    writeln!(out, "{indent} */").ok();
1816}
1817
1818/// Maximum line length before splitting record fields across multiple lines.
1819/// Checkstyle enforces 120 chars; we split at 100 to leave headroom for indentation.
1820const RECORD_LINE_WRAP_THRESHOLD: usize = 100;
1821
1822fn gen_record_type(package: &str, typ: &TypeDef, complex_enums: &AHashSet<String>, lang_rename_all: &str) -> String {
1823    // Generate the record body first, then scan for needed imports.
1824    // For each field, if the language uses camelCase but the JSON key is snake_case
1825    // (the Rust default), annotate with @JsonProperty so Jackson maps correctly.
1826    // Also collect per-field doc strings for Javadoc emission.
1827    let (field_list, field_docs): (Vec<String>, Vec<String>) = typ
1828        .fields
1829        .iter()
1830        .map(|f| {
1831            // Complex enums (tagged unions with data) can't be simple Java enums.
1832            // Use Object for flexible Jackson deserialization.
1833            let is_complex = matches!(&f.ty, TypeRef::Named(n) if complex_enums.contains(n.as_str()));
1834            let ftype = if is_complex {
1835                "Object".to_string()
1836            } else if f.optional {
1837                format!("Optional<{}>", java_boxed_type(&f.ty))
1838            } else {
1839                java_type(&f.ty).to_string()
1840            };
1841            let jname = safe_java_field_name(&f.name);
1842            // When the language convention is camelCase but the JSON wire format uses
1843            // snake_case (the Rust/serde default), add an explicit @JsonProperty annotation
1844            // so Jackson serialises/deserialises using the correct snake_case key.
1845            let decl = if lang_rename_all == "camelCase" && f.name.contains('_') {
1846                format!("@JsonProperty(\"{}\") {} {}", f.name, ftype, jname)
1847            } else {
1848                format!("{} {}", ftype, jname)
1849            };
1850            (decl, f.doc.clone())
1851        })
1852        .unzip();
1853
1854    // Build the single-line form to check length and scan for imports.
1855    // Doc strings are intentionally excluded from this check so the threshold
1856    // stays stable regardless of documentation presence.
1857    let single_line = format!("public record {}({}) {{ }}", typ.name, field_list.join(", "));
1858
1859    // Build the actual record declaration, splitting across lines if too long.
1860    let mut record_block = String::new();
1861    emit_javadoc(&mut record_block, &typ.doc, "");
1862    if single_line.len() > RECORD_LINE_WRAP_THRESHOLD && field_list.len() > 1 {
1863        writeln!(record_block, "public record {}(", typ.name).ok();
1864        for (i, (field, doc)) in field_list.iter().zip(field_docs.iter()).enumerate() {
1865            let comma = if i < field_list.len() - 1 { "," } else { "" };
1866            if !doc.is_empty() {
1867                // Inline single-line doc for record components in multi-line form.
1868                let doc_summary = escape_javadoc_line(doc.lines().next().unwrap_or("").trim());
1869                writeln!(record_block, "    /** {doc_summary} */").ok();
1870            }
1871            writeln!(record_block, "    {}{}", field, comma).ok();
1872        }
1873        writeln!(record_block, ") {{").ok();
1874    } else {
1875        writeln!(record_block, "public record {}({}) {{", typ.name, field_list.join(", ")).ok();
1876    }
1877
1878    // Add builder() factory method if type has defaults
1879    if typ.has_default {
1880        writeln!(record_block, "    public static {}Builder builder() {{", typ.name).ok();
1881        writeln!(record_block, "        return new {}Builder();", typ.name).ok();
1882        writeln!(record_block, "    }}").ok();
1883    }
1884
1885    writeln!(record_block, "}}").ok();
1886
1887    // Scan the single-line form to determine which imports are needed
1888    let needs_json_property = field_list.iter().any(|f| f.contains("@JsonProperty("));
1889    let mut out = String::with_capacity(record_block.len() + 512);
1890    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1891    writeln!(out, "package {};", package).ok();
1892    writeln!(out).ok();
1893    if single_line.contains("List<") {
1894        writeln!(out, "import java.util.List;").ok();
1895    }
1896    if single_line.contains("Map<") {
1897        writeln!(out, "import java.util.Map;").ok();
1898    }
1899    if single_line.contains("Optional<") {
1900        writeln!(out, "import java.util.Optional;").ok();
1901    }
1902    if needs_json_property {
1903        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
1904    }
1905    writeln!(out).ok();
1906    write!(out, "{}", record_block).ok();
1907
1908    out
1909}
1910
1911// ---------------------------------------------------------------------------
1912// Enum classes
1913// ---------------------------------------------------------------------------
1914
1915/// Apply a serde `rename_all` strategy to a variant name for Java codegen.
1916fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1917    match rename_all {
1918        Some("snake_case") => name.to_snake_case(),
1919        Some("camelCase") => name.to_lower_camel_case(),
1920        Some("PascalCase") => name.to_pascal_case(),
1921        Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1922        Some("lowercase") => name.to_lowercase(),
1923        Some("UPPERCASE") => name.to_uppercase(),
1924        _ => name.to_lowercase(),
1925    }
1926}
1927
1928fn gen_enum_class(package: &str, enum_def: &EnumDef) -> String {
1929    let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1930
1931    // Tagged union: enum has a serde tag AND data variants → generate sealed interface hierarchy
1932    if enum_def.serde_tag.is_some() && has_data_variants {
1933        return gen_java_tagged_union(package, enum_def);
1934    }
1935
1936    let mut out = String::with_capacity(1024);
1937
1938    out.push_str(&hash::header(CommentStyle::DoubleSlash));
1939    writeln!(out, "package {};", package).ok();
1940    writeln!(out).ok();
1941    writeln!(out, "import com.fasterxml.jackson.annotation.JsonCreator;").ok();
1942    writeln!(out, "import com.fasterxml.jackson.annotation.JsonValue;").ok();
1943    writeln!(out).ok();
1944
1945    emit_javadoc(&mut out, &enum_def.doc, "");
1946    writeln!(out, "public enum {} {{", enum_def.name).ok();
1947
1948    for (i, variant) in enum_def.variants.iter().enumerate() {
1949        let comma = if i < enum_def.variants.len() - 1 { "," } else { ";" };
1950        // Use serde_rename if available, otherwise apply rename_all strategy
1951        let json_name = variant
1952            .serde_rename
1953            .clone()
1954            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1955        if !variant.doc.is_empty() {
1956            let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
1957            // 4 spaces indent + "/** " + " */" = 11 chars overhead; wrap if total > 80
1958            if doc_summary.len() + 11 > 80 {
1959                writeln!(out, "    /**").ok();
1960                writeln!(out, "     * {doc_summary}").ok();
1961                writeln!(out, "     */").ok();
1962            } else {
1963                writeln!(out, "    /** {doc_summary} */").ok();
1964            }
1965        }
1966        writeln!(out, "    {}(\"{}\"){}", variant.name, json_name, comma).ok();
1967    }
1968
1969    writeln!(out).ok();
1970    writeln!(out, "    /** The string value. */").ok();
1971    writeln!(out, "    private final String value;").ok();
1972    writeln!(out).ok();
1973    writeln!(out, "    {}(final String value) {{", enum_def.name).ok();
1974    writeln!(out, "        this.value = value;").ok();
1975    writeln!(out, "    }}").ok();
1976    writeln!(out).ok();
1977    writeln!(out, "    /** Returns the string value. */").ok();
1978    writeln!(out, "    @JsonValue").ok();
1979    writeln!(out, "    public String getValue() {{").ok();
1980    writeln!(out, "        return value;").ok();
1981    writeln!(out, "    }}").ok();
1982    writeln!(out).ok();
1983    writeln!(out, "    /** Creates an instance from a string value. */").ok();
1984    writeln!(out, "    @JsonCreator").ok();
1985    writeln!(
1986        out,
1987        "    public static {} fromValue(final String value) {{",
1988        enum_def.name
1989    )
1990    .ok();
1991    writeln!(out, "        for ({} e : values()) {{", enum_def.name).ok();
1992    writeln!(out, "            if (e.value.equalsIgnoreCase(value)) {{").ok();
1993    writeln!(out, "                return e;").ok();
1994    writeln!(out, "            }}").ok();
1995    writeln!(out, "        }}").ok();
1996    writeln!(
1997        out,
1998        "        throw new IllegalArgumentException(\"Unknown value: \" + value);"
1999    )
2000    .ok();
2001    writeln!(out, "    }}").ok();
2002
2003    writeln!(out, "}}").ok();
2004
2005    out
2006}
2007
2008/// Generate a Java sealed interface hierarchy for internally tagged enums.
2009///
2010/// Maps `#[serde(tag = "type_field", rename_all = "snake_case")]` Rust enums to
2011/// `@JsonTypeInfo` / `@JsonSubTypes` Java sealed interfaces with record implementations.
2012fn gen_java_tagged_union(package: &str, enum_def: &EnumDef) -> String {
2013    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
2014
2015    // Collect variant names to detect Java type name conflicts.
2016    // If a variant is named "List", "Map", or "Optional", using those type names
2017    // inside the sealed interface would refer to the nested record, not java.util.*.
2018    // We use fully qualified names in that case.
2019    let variant_names: std::collections::HashSet<&str> = enum_def.variants.iter().map(|v| v.name.as_str()).collect();
2020    let optional_type = if variant_names.contains("Optional") {
2021        "java.util.Optional"
2022    } else {
2023        "Optional"
2024    };
2025
2026    // @JsonProperty is only needed for variants with named (non-tuple) fields.
2027    let needs_json_property = enum_def
2028        .variants
2029        .iter()
2030        .any(|v| v.fields.iter().any(|f| !is_tuple_field_name(&f.name)));
2031
2032    let mut out = String::with_capacity(2048);
2033    out.push_str(&hash::header(CommentStyle::DoubleSlash));
2034    writeln!(out, "package {};", package).ok();
2035    writeln!(out).ok();
2036    if needs_json_property {
2037        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
2038    }
2039    writeln!(out, "import com.fasterxml.jackson.annotation.JsonSubTypes;").ok();
2040    writeln!(out, "import com.fasterxml.jackson.annotation.JsonTypeInfo;").ok();
2041
2042    // Check if any field types need list/map/optional imports (only when not conflicting)
2043    let needs_list = !variant_names.contains("List")
2044        && enum_def
2045            .variants
2046            .iter()
2047            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Vec(_))));
2048    let needs_map = !variant_names.contains("Map")
2049        && enum_def
2050            .variants
2051            .iter()
2052            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))));
2053    let needs_optional =
2054        !variant_names.contains("Optional") && enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.optional));
2055    // Newtype/tuple variants (field name is a numeric index like "0") are flattened
2056    // into the parent JSON object using @JsonUnwrapped.
2057    let needs_unwrapped = enum_def
2058        .variants
2059        .iter()
2060        .any(|v| v.fields.len() == 1 && is_tuple_field_name(&v.fields[0].name));
2061    if needs_list {
2062        writeln!(out, "import java.util.List;").ok();
2063    }
2064    if needs_map {
2065        writeln!(out, "import java.util.Map;").ok();
2066    }
2067    if needs_optional {
2068        writeln!(out, "import java.util.Optional;").ok();
2069    }
2070    if needs_unwrapped {
2071        writeln!(out, "import com.fasterxml.jackson.annotation.JsonUnwrapped;").ok();
2072    }
2073    writeln!(out).ok();
2074
2075    emit_javadoc(&mut out, &enum_def.doc, "");
2076    // @JsonTypeInfo and @JsonSubTypes annotations
2077    writeln!(
2078        out,
2079        "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"{tag_field}\", visible = false)"
2080    )
2081    .ok();
2082    writeln!(out, "@JsonSubTypes({{").ok();
2083    for (i, variant) in enum_def.variants.iter().enumerate() {
2084        let discriminator = variant
2085            .serde_rename
2086            .clone()
2087            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2088        let comma = if i < enum_def.variants.len() - 1 { "," } else { "" };
2089        writeln!(
2090            out,
2091            "    @JsonSubTypes.Type(value = {}.{}.class, name = \"{}\"){}",
2092            enum_def.name, variant.name, discriminator, comma
2093        )
2094        .ok();
2095    }
2096    writeln!(out, "}})").ok();
2097    writeln!(out, "public sealed interface {} {{", enum_def.name).ok();
2098
2099    // Nested records for each variant
2100    for variant in &enum_def.variants {
2101        writeln!(out).ok();
2102        if variant.fields.is_empty() {
2103            // Unit variant
2104            if !variant.doc.is_empty() {
2105                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2106                writeln!(out, "    /** {doc_summary} */").ok();
2107            }
2108            writeln!(out, "    record {}() implements {} {{", variant.name, enum_def.name).ok();
2109            writeln!(out, "    }}").ok();
2110        } else {
2111            // Build field list using fully qualified names where variant names shadow imports
2112            let field_parts: Vec<String> = variant
2113                .fields
2114                .iter()
2115                .map(|f| {
2116                    let ftype = if f.optional {
2117                        let inner = java_boxed_type(&f.ty);
2118                        let inner_str = inner.as_ref();
2119                        // Replace "List"/"Map" with fully qualified if conflicting
2120                        let inner_qualified = if inner_str.starts_with("List<") && variant_names.contains("List") {
2121                            inner_str.replacen("List<", "java.util.List<", 1)
2122                        } else if inner_str.starts_with("Map<") && variant_names.contains("Map") {
2123                            inner_str.replacen("Map<", "java.util.Map<", 1)
2124                        } else {
2125                            inner_str.to_string()
2126                        };
2127                        format!("{optional_type}<{inner_qualified}>")
2128                    } else {
2129                        let t = java_type(&f.ty);
2130                        let t_str = t.as_ref();
2131                        if t_str.starts_with("List<") && variant_names.contains("List") {
2132                            t_str.replacen("List<", "java.util.List<", 1)
2133                        } else if t_str.starts_with("Map<") && variant_names.contains("Map") {
2134                            t_str.replacen("Map<", "java.util.Map<", 1)
2135                        } else {
2136                            t_str.to_string()
2137                        }
2138                    };
2139                    // Tuple/newtype variants have numeric field names (e.g. "0", "_0").
2140                    // These are not real JSON keys — serde flattens the inner type's fields
2141                    // alongside the tag. Use @JsonUnwrapped so Jackson does the same.
2142                    if is_tuple_field_name(&f.name) {
2143                        format!("@JsonUnwrapped {ftype} value")
2144                    } else {
2145                        let json_name = f.name.trim_start_matches('_');
2146                        let jname = safe_java_field_name(json_name);
2147                        format!("@JsonProperty(\"{json_name}\") {ftype} {jname}")
2148                    }
2149                })
2150                .collect();
2151
2152            let single = format!(
2153                "    record {}({}) implements {} {{ }}",
2154                variant.name,
2155                field_parts.join(", "),
2156                enum_def.name
2157            );
2158
2159            if !variant.doc.is_empty() {
2160                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2161                writeln!(out, "    /** {doc_summary} */").ok();
2162            }
2163            if single.len() > RECORD_LINE_WRAP_THRESHOLD && field_parts.len() > 1 {
2164                writeln!(out, "    record {}(", variant.name).ok();
2165                for (i, fp) in field_parts.iter().enumerate() {
2166                    let comma = if i < field_parts.len() - 1 { "," } else { "" };
2167                    writeln!(out, "        {}{}", fp, comma).ok();
2168                }
2169                writeln!(out, "    ) implements {} {{", enum_def.name).ok();
2170                writeln!(out, "    }}").ok();
2171            } else {
2172                writeln!(
2173                    out,
2174                    "    record {}({}) implements {} {{ }}",
2175                    variant.name,
2176                    field_parts.join(", "),
2177                    enum_def.name
2178                )
2179                .ok();
2180            }
2181        }
2182    }
2183
2184    writeln!(out).ok();
2185    writeln!(out, "}}").ok();
2186    out
2187}
2188
2189// ---------------------------------------------------------------------------
2190// Helper functions for FFI marshalling
2191// ---------------------------------------------------------------------------
2192
2193fn gen_ffi_layout(ty: &TypeRef) -> String {
2194    match ty {
2195        TypeRef::Primitive(prim) => java_ffi_type(prim).to_string(),
2196        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "ValueLayout.ADDRESS".to_string(),
2197        TypeRef::Bytes => "ValueLayout.ADDRESS".to_string(),
2198        TypeRef::Optional(inner) => gen_ffi_layout(inner),
2199        TypeRef::Vec(_) => "ValueLayout.ADDRESS".to_string(),
2200        TypeRef::Map(_, _) => "ValueLayout.ADDRESS".to_string(),
2201        TypeRef::Named(_) => "ValueLayout.ADDRESS".to_string(),
2202        TypeRef::Unit => "".to_string(),
2203        TypeRef::Duration => "ValueLayout.JAVA_LONG".to_string(),
2204    }
2205}
2206
2207fn marshal_param_to_ffi(out: &mut String, name: &str, ty: &TypeRef, opaque_types: &AHashSet<String>, prefix: &str) {
2208    match ty {
2209        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2210            let cname = "c".to_string() + name;
2211            writeln!(out, "            var {} = arena.allocateFrom({});", cname, name).ok();
2212        }
2213        TypeRef::Named(type_name) => {
2214            let cname = "c".to_string() + name;
2215            if opaque_types.contains(type_name.as_str()) {
2216                // Opaque handles: pass the inner MemorySegment via .handle()
2217                writeln!(out, "            var {} = {}.handle();", cname, name).ok();
2218            } else {
2219                // Non-opaque named types: serialize to JSON, call _from_json to get FFI pointer.
2220                // The pointer must be freed after the FFI call with _free.
2221                let type_snake = type_name.to_snake_case();
2222                let from_json_handle = format!(
2223                    "NativeLib.{}_{}_FROM_JSON",
2224                    prefix.to_uppercase(),
2225                    type_snake.to_uppercase()
2226                );
2227                let _free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
2228                writeln!(
2229                    out,
2230                    "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2231                    cname, name, name
2232                )
2233                .ok();
2234                writeln!(
2235                    out,
2236                    "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;",
2237                    cname, cname, cname
2238                )
2239                .ok();
2240                writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2241                writeln!(
2242                    out,
2243                    "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2244                    from_json_handle, cname
2245                )
2246                .ok();
2247                writeln!(out, "                : MemorySegment.NULL;").ok();
2248            }
2249        }
2250        TypeRef::Optional(inner) => {
2251            // For optional types, marshal the inner type if not null
2252            match inner.as_ref() {
2253                TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2254                    let cname = "c".to_string() + name;
2255                    writeln!(
2256                        out,
2257                        "            var {} = {} != null ? arena.allocateFrom({}) : MemorySegment.NULL;",
2258                        cname, name, name
2259                    )
2260                    .ok();
2261                }
2262                TypeRef::Named(type_name) => {
2263                    let cname = "c".to_string() + name;
2264                    if opaque_types.contains(type_name.as_str()) {
2265                        writeln!(
2266                            out,
2267                            "            var {} = {} != null ? {}.handle() : MemorySegment.NULL;",
2268                            cname, name, name
2269                        )
2270                        .ok();
2271                    } else {
2272                        // Non-opaque named type in Optional: serialize to JSON and call _from_json
2273                        let type_snake = type_name.to_snake_case();
2274                        let from_json_handle = format!(
2275                            "NativeLib.{}_{}_FROM_JSON",
2276                            prefix.to_uppercase(),
2277                            type_snake.to_uppercase()
2278                        );
2279                        writeln!(
2280                            out,
2281                            "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2282                            cname, name, name
2283                        )
2284                        .ok();
2285                        writeln!(out, "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;", cname, cname, cname).ok();
2286                        writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2287                        writeln!(
2288                            out,
2289                            "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2290                            from_json_handle, cname
2291                        )
2292                        .ok();
2293                        writeln!(out, "                : MemorySegment.NULL;").ok();
2294                    }
2295                }
2296                _ => {
2297                    // Other optional types (primitives) pass through
2298                }
2299            }
2300        }
2301        TypeRef::Vec(_) | TypeRef::Map(_, _) => {
2302            // Vec/Map types: serialize to JSON string, then pass as a C string via arena.
2303            let cname = "c".to_string() + name;
2304            writeln!(
2305                out,
2306                "            var {}Json = createObjectMapper().writeValueAsString({});",
2307                cname, name
2308            )
2309            .ok();
2310            writeln!(out, "            var {} = arena.allocateFrom({}Json);", cname, cname).ok();
2311        }
2312        _ => {
2313            // Primitives and others pass through directly
2314        }
2315    }
2316}
2317
2318fn ffi_param_name(name: &str, ty: &TypeRef, _opaque_types: &AHashSet<String>) -> String {
2319    match ty {
2320        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "c".to_string() + name,
2321        TypeRef::Named(_) => "c".to_string() + name,
2322        TypeRef::Vec(_) | TypeRef::Map(_, _) => "c".to_string() + name,
2323        TypeRef::Optional(inner) => match inner.as_ref() {
2324            TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Named(_) => {
2325                "c".to_string() + name
2326            }
2327            _ => name.to_string(),
2328        },
2329        _ => name.to_string(),
2330    }
2331}
2332
2333/// Build a `FunctionDescriptor` string for a given return layout and parameter layouts.
2334/// Handles void returns (ofVoid) and non-void returns (of) correctly.
2335fn gen_function_descriptor(return_layout: &str, param_layouts: &[String]) -> String {
2336    if return_layout.is_empty() {
2337        // Void return
2338        if param_layouts.is_empty() {
2339            "FunctionDescriptor.ofVoid()".to_string()
2340        } else {
2341            format!("FunctionDescriptor.ofVoid({})", param_layouts.join(", "))
2342        }
2343    } else {
2344        // Non-void return
2345        if param_layouts.is_empty() {
2346            format!("FunctionDescriptor.of({})", return_layout)
2347        } else {
2348            format!("FunctionDescriptor.of({}, {})", return_layout, param_layouts.join(", "))
2349        }
2350    }
2351}
2352
2353/// Returns true if the given return type maps to an FFI ADDRESS that represents a string
2354/// (i.e. the FFI returns `*mut c_char` which must be unmarshaled and freed).
2355fn is_ffi_string_return(ty: &TypeRef) -> bool {
2356    match ty {
2357        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => true,
2358        TypeRef::Optional(inner) => is_ffi_string_return(inner),
2359        _ => false,
2360    }
2361}
2362
2363/// Returns the appropriate Java cast type for non-string FFI return values.
2364fn java_ffi_return_cast(ty: &TypeRef) -> &'static str {
2365    match ty {
2366        TypeRef::Primitive(prim) => match prim {
2367            PrimitiveType::Bool => "boolean",
2368            PrimitiveType::U8 | PrimitiveType::I8 => "byte",
2369            PrimitiveType::U16 | PrimitiveType::I16 => "short",
2370            PrimitiveType::U32 | PrimitiveType::I32 => "int",
2371            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "long",
2372            PrimitiveType::F32 => "float",
2373            PrimitiveType::F64 => "double",
2374        },
2375        TypeRef::Bytes | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) => "MemorySegment",
2376        _ => "MemorySegment",
2377    }
2378}
2379
2380fn gen_helper_methods(out: &mut String, prefix: &str, class_name: &str) {
2381    // Only emit helper methods that are actually called in the generated body.
2382    let needs_check_last_error = out.contains("checkLastError()");
2383    let needs_read_cstring = out.contains("readCString(");
2384    let needs_read_bytes = out.contains("readBytes(");
2385    let needs_create_object_mapper = out.contains("createObjectMapper()");
2386
2387    if !needs_check_last_error && !needs_read_cstring && !needs_read_bytes && !needs_create_object_mapper {
2388        return;
2389    }
2390
2391    writeln!(out, "    // Helper methods for FFI marshalling").ok();
2392    writeln!(out).ok();
2393
2394    if needs_check_last_error {
2395        // Reads the last FFI error code and, if non-zero, reads the error message and throws.
2396        // Called immediately after a null-pointer return from an FFI call.
2397        writeln!(out, "    private static void checkLastError() throws Throwable {{").ok();
2398        writeln!(
2399            out,
2400            "        int errCode = (int) NativeLib.{}_LAST_ERROR_CODE.invoke();",
2401            prefix.to_uppercase()
2402        )
2403        .ok();
2404        writeln!(out, "        if (errCode != 0) {{").ok();
2405        writeln!(
2406            out,
2407            "            var ctxPtr = (MemorySegment) NativeLib.{}_LAST_ERROR_CONTEXT.invoke();",
2408            prefix.to_uppercase()
2409        )
2410        .ok();
2411        writeln!(
2412            out,
2413            "            String msg = ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);"
2414        )
2415        .ok();
2416        writeln!(out, "            throw new {}Exception(errCode, msg);", class_name).ok();
2417        writeln!(out, "        }}").ok();
2418        writeln!(out, "    }}").ok();
2419        writeln!(out).ok();
2420    }
2421
2422    if needs_create_object_mapper {
2423        // Emit a configured ObjectMapper factory:
2424        //   - findAndRegisterModules() to pick up jackson-datatype-jdk8 (Optional support)
2425        //   - ACCEPT_CASE_INSENSITIVE_ENUMS so enum names like "json_ld" match JsonLd, etc.
2426        // Field name mapping relies on explicit @JsonProperty annotations on record components
2427        // (generated by alef for snake_case FFI fields on camelCase Java records).
2428        writeln!(
2429            out,
2430            "    private static com.fasterxml.jackson.databind.ObjectMapper createObjectMapper() {{"
2431        )
2432        .ok();
2433        writeln!(out, "        return new com.fasterxml.jackson.databind.ObjectMapper()").ok();
2434        writeln!(
2435            out,
2436            "            .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())"
2437        )
2438        .ok();
2439        writeln!(out, "            .findAndRegisterModules()").ok();
2440        writeln!(
2441            out,
2442            "            .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)"
2443        )
2444        .ok();
2445        writeln!(
2446            out,
2447            "            .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);"
2448        )
2449        .ok();
2450        writeln!(out, "    }}").ok();
2451        writeln!(out).ok();
2452    }
2453
2454    if needs_read_cstring {
2455        writeln!(out, "    private static String readCString(MemorySegment ptr) {{").ok();
2456        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2457        writeln!(out, "            return null;").ok();
2458        writeln!(out, "        }}").ok();
2459        writeln!(out, "        return ptr.getUtf8String(0);").ok();
2460        writeln!(out, "    }}").ok();
2461        writeln!(out).ok();
2462    }
2463
2464    if needs_read_bytes {
2465        writeln!(
2466            out,
2467            "    private static byte[] readBytes(MemorySegment ptr, long len) {{"
2468        )
2469        .ok();
2470        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2471        writeln!(out, "            return new byte[0];").ok();
2472        writeln!(out, "        }}").ok();
2473        writeln!(out, "        byte[] bytes = new byte[(int) len];").ok();
2474        writeln!(
2475            out,
2476            "        MemorySegment.copy(ptr, ValueLayout.JAVA_BYTE.byteSize() * 0, bytes, 0, (int) len);"
2477        )
2478        .ok();
2479        writeln!(out, "        return bytes;").ok();
2480        writeln!(out, "    }}").ok();
2481    }
2482}
2483
2484// ---------------------------------------------------------------------------
2485// Builder class for types with defaults
2486// ---------------------------------------------------------------------------
2487
2488/// Format a default value for an Optional field, wrapping it in Optional.of()
2489/// with proper Java literal syntax.
2490fn format_optional_value(ty: &TypeRef, default: &str) -> String {
2491    // Check if the default is already wrapped (e.g., "Optional.of(...)" or "Optional.empty()")
2492    if default.contains("Optional.") {
2493        return default.to_string();
2494    }
2495
2496    // Unwrap Optional types to get the inner type
2497    let inner_ty = match ty {
2498        TypeRef::Optional(inner) => inner.as_ref(),
2499        other => other,
2500    };
2501
2502    // Determine the proper literal suffix based on type
2503    let formatted_value = match inner_ty {
2504        TypeRef::Primitive(p) => match p {
2505            PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
2506                // Add 'L' suffix for long values if not already present
2507                if default.ends_with('L') || default.ends_with('l') {
2508                    default.to_string()
2509                } else if default.parse::<i64>().is_ok() {
2510                    format!("{}L", default)
2511                } else {
2512                    default.to_string()
2513                }
2514            }
2515            PrimitiveType::F32 => {
2516                // Add 'f' suffix for float values if not already present
2517                if default.ends_with('f') || default.ends_with('F') {
2518                    default.to_string()
2519                } else if default.parse::<f32>().is_ok() {
2520                    format!("{}f", default)
2521                } else {
2522                    default.to_string()
2523                }
2524            }
2525            PrimitiveType::F64 => {
2526                // Double defaults can have optional 'd' suffix, but 0.0 is fine
2527                default.to_string()
2528            }
2529            _ => default.to_string(),
2530        },
2531        _ => default.to_string(),
2532    };
2533
2534    format!("Optional.of({})", formatted_value)
2535}
2536
2537fn gen_builder_class(package: &str, typ: &TypeDef) -> String {
2538    let mut body = String::with_capacity(2048);
2539
2540    emit_javadoc(&mut body, &typ.doc, "");
2541    writeln!(body, "public class {}Builder {{", typ.name).ok();
2542    writeln!(body).ok();
2543
2544    // Generate field declarations with defaults
2545    for field in &typ.fields {
2546        let field_name = safe_java_field_name(&field.name);
2547
2548        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2549        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2550            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2551        {
2552            continue;
2553        }
2554
2555        // Duration maps to primitive `long` in the public record, but in builder
2556        // classes we use boxed `Long` so that `null` can represent "not set".
2557        let field_type = if field.optional {
2558            format!("Optional<{}>", java_boxed_type(&field.ty))
2559        } else if matches!(field.ty, TypeRef::Duration) {
2560            java_boxed_type(&field.ty).to_string()
2561        } else {
2562            java_type(&field.ty).to_string()
2563        };
2564
2565        let default_value = if field.optional {
2566            // For Optional fields, always use Optional.empty() or Optional.of(value)
2567            if let Some(default) = &field.default {
2568                // If there's an explicit default, wrap it in Optional.of()
2569                format_optional_value(&field.ty, default)
2570            } else {
2571                // If no default, use Optional.empty()
2572                "Optional.empty()".to_string()
2573            }
2574        } else {
2575            // For non-Optional fields, use regular defaults
2576            if let Some(default) = &field.default {
2577                default.clone()
2578            } else {
2579                match &field.ty {
2580                    TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
2581                    TypeRef::Json => "null".to_string(),
2582                    TypeRef::Bytes => "new byte[0]".to_string(),
2583                    TypeRef::Primitive(p) => match p {
2584                        PrimitiveType::Bool => "false".to_string(),
2585                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
2586                        _ => "0".to_string(),
2587                    },
2588                    TypeRef::Vec(_) => "List.of()".to_string(),
2589                    TypeRef::Map(_, _) => "Map.of()".to_string(),
2590                    TypeRef::Optional(_) => "Optional.empty()".to_string(),
2591                    TypeRef::Duration => "null".to_string(),
2592                    _ => "null".to_string(),
2593                }
2594            }
2595        };
2596
2597        writeln!(body, "    private {} {} = {};", field_type, field_name, default_value).ok();
2598    }
2599
2600    writeln!(body).ok();
2601
2602    // Generate withXxx() methods
2603    for field in &typ.fields {
2604        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2605        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2606            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2607        {
2608            continue;
2609        }
2610
2611        let field_name = safe_java_field_name(&field.name);
2612        let field_name_pascal = to_class_name(&field.name);
2613        let field_type = if field.optional {
2614            format!("Optional<{}>", java_boxed_type(&field.ty))
2615        } else if matches!(field.ty, TypeRef::Duration) {
2616            java_boxed_type(&field.ty).to_string()
2617        } else {
2618            java_type(&field.ty).to_string()
2619        };
2620
2621        writeln!(body, "    /** Sets the {} field. */", field_name).ok();
2622        writeln!(
2623            body,
2624            "    public {}Builder with{}(final {} value) {{",
2625            typ.name, field_name_pascal, field_type
2626        )
2627        .ok();
2628        writeln!(body, "        this.{} = value;", field_name).ok();
2629        writeln!(body, "        return this;").ok();
2630        writeln!(body, "    }}").ok();
2631        writeln!(body).ok();
2632    }
2633
2634    // Generate build() method
2635    writeln!(body, "    /** Builds the {} instance. */", typ.name).ok();
2636    writeln!(body, "    public {} build() {{", typ.name).ok();
2637    writeln!(body, "        return new {}(", typ.name).ok();
2638    let non_tuple_fields: Vec<_> = typ
2639        .fields
2640        .iter()
2641        .filter(|f| {
2642            // Include named fields (skip unnamed tuple fields)
2643            !(f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit())
2644                || f.name.chars().next().is_none_or(|c| c.is_ascii_digit()))
2645        })
2646        .collect();
2647    for (i, field) in non_tuple_fields.iter().enumerate() {
2648        let field_name = safe_java_field_name(&field.name);
2649        let comma = if i < non_tuple_fields.len() - 1 { "," } else { "" };
2650        writeln!(body, "            {}{}", field_name, comma).ok();
2651    }
2652    writeln!(body, "        );").ok();
2653    writeln!(body, "    }}").ok();
2654
2655    writeln!(body, "}}").ok();
2656
2657    // Now assemble with conditional imports based on what's actually used in the body
2658    let mut out = String::with_capacity(body.len() + 512);
2659
2660    out.push_str(&hash::header(CommentStyle::DoubleSlash));
2661    writeln!(out, "package {};", package).ok();
2662    writeln!(out).ok();
2663
2664    if body.contains("List<") {
2665        writeln!(out, "import java.util.List;").ok();
2666    }
2667    if body.contains("Map<") {
2668        writeln!(out, "import java.util.Map;").ok();
2669    }
2670    if body.contains("Optional<") {
2671        writeln!(out, "import java.util.Optional;").ok();
2672    }
2673
2674    writeln!(out).ok();
2675    out.push_str(&body);
2676
2677    out
2678}