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