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!("{} {}", 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!("{} {}", 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, "public class {}Exception extends Exception {{", class_name).ok();
1468    writeln!(out, "    private final int code;").ok();
1469    writeln!(out).ok();
1470    writeln!(out, "    public {}Exception(int code, String message) {{", class_name).ok();
1471    writeln!(out, "        super(message);").ok();
1472    writeln!(out, "        this.code = code;").ok();
1473    writeln!(out, "    }}").ok();
1474    writeln!(out).ok();
1475    writeln!(
1476        out,
1477        "    public {}Exception(String message, Throwable cause) {{",
1478        class_name
1479    )
1480    .ok();
1481    writeln!(out, "        super(message, cause);").ok();
1482    writeln!(out, "        this.code = -1;").ok();
1483    writeln!(out, "    }}").ok();
1484    writeln!(out).ok();
1485    writeln!(out, "    public int getCode() {{").ok();
1486    writeln!(out, "        return code;").ok();
1487    writeln!(out, "    }}").ok();
1488    writeln!(out, "}}").ok();
1489
1490    out
1491}
1492
1493// ---------------------------------------------------------------------------
1494// High-level facade class (public API)
1495// ---------------------------------------------------------------------------
1496
1497#[allow(clippy::too_many_arguments)]
1498fn gen_facade_class(
1499    api: &ApiSurface,
1500    package: &str,
1501    public_class: &str,
1502    raw_class: &str,
1503    _prefix: &str,
1504    bridge_param_names: &HashSet<String>,
1505    bridge_type_aliases: &HashSet<String>,
1506    has_visitor_bridge: bool,
1507) -> String {
1508    let mut body = String::with_capacity(4096);
1509
1510    writeln!(body, "public final class {} {{", public_class).ok();
1511    writeln!(body, "    private {}() {{ }}", public_class).ok();
1512    writeln!(body).ok();
1513
1514    // Generate static methods for free functions
1515    for func in &api.functions {
1516        // Sync method — bridge params stripped from public signature
1517        let params: Vec<String> = func
1518            .params
1519            .iter()
1520            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1521            .map(|p| {
1522                let ptype = java_type(&p.ty);
1523                format!("{} {}", ptype, to_java_name(&p.name))
1524            })
1525            .collect();
1526
1527        let return_type = java_type(&func.return_type);
1528
1529        if !func.doc.is_empty() {
1530            writeln!(body, "    /**").ok();
1531            for line in func.doc.lines() {
1532                writeln!(body, "     * {}", line).ok();
1533            }
1534            writeln!(body, "     */").ok();
1535        }
1536
1537        writeln!(
1538            body,
1539            "    public static {} {}({}) throws {}Exception {{",
1540            return_type,
1541            to_java_name(&func.name),
1542            params.join(", "),
1543            raw_class
1544        )
1545        .ok();
1546
1547        // Null checks for required non-bridge parameters
1548        for param in &func.params {
1549            if !param.optional && !is_bridge_param_java(param, bridge_param_names, bridge_type_aliases) {
1550                let pname = to_java_name(&param.name);
1551                writeln!(
1552                    body,
1553                    "        java.util.Objects.requireNonNull({}, \"{} must not be null\");",
1554                    pname, pname
1555                )
1556                .ok();
1557            }
1558        }
1559
1560        // Delegate to raw FFI class — bridge params are stripped from the raw class
1561        // signature, so we must exclude them entirely (not pass null) to match the
1562        // raw class's parameter count.
1563        let call_args: Vec<String> = func
1564            .params
1565            .iter()
1566            .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1567            .map(|p| to_java_name(&p.name))
1568            .collect();
1569
1570        if matches!(func.return_type, TypeRef::Unit) {
1571            writeln!(
1572                body,
1573                "        {}.{}({});",
1574                raw_class,
1575                to_java_name(&func.name),
1576                call_args.join(", ")
1577            )
1578            .ok();
1579        } else {
1580            writeln!(
1581                body,
1582                "        return {}.{}({});",
1583                raw_class,
1584                to_java_name(&func.name),
1585                call_args.join(", ")
1586            )
1587            .ok();
1588        }
1589
1590        writeln!(body, "    }}").ok();
1591        writeln!(body).ok();
1592
1593        // Generate overload without optional params (convenience method).
1594        // Only non-bridge params are considered here.
1595        let has_optional = func
1596            .params
1597            .iter()
1598            .any(|p| p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases));
1599        if has_optional {
1600            let required_params: Vec<String> = func
1601                .params
1602                .iter()
1603                .filter(|p| !p.optional && !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1604                .map(|p| {
1605                    let ptype = java_type(&p.ty);
1606                    format!("{} {}", ptype, to_java_name(&p.name))
1607                })
1608                .collect();
1609
1610            writeln!(
1611                body,
1612                "    public static {} {}({}) throws {}Exception {{",
1613                return_type,
1614                to_java_name(&func.name),
1615                required_params.join(", "),
1616                raw_class
1617            )
1618            .ok();
1619
1620            // Build call to raw class: bridge params are excluded (stripped from raw
1621            // class signature), optional params passed as null.
1622            let full_args: Vec<String> = func
1623                .params
1624                .iter()
1625                .filter(|p| !is_bridge_param_java(p, bridge_param_names, bridge_type_aliases))
1626                .map(|p| {
1627                    if p.optional {
1628                        "null".to_string()
1629                    } else {
1630                        to_java_name(&p.name)
1631                    }
1632                })
1633                .collect();
1634
1635            if matches!(func.return_type, TypeRef::Unit) {
1636                writeln!(
1637                    body,
1638                    "        {}.{}({});",
1639                    raw_class,
1640                    to_java_name(&func.name),
1641                    full_args.join(", ")
1642                )
1643                .ok();
1644            } else {
1645                writeln!(
1646                    body,
1647                    "        return {}.{}({});",
1648                    raw_class,
1649                    to_java_name(&func.name),
1650                    full_args.join(", ")
1651                )
1652                .ok();
1653            }
1654
1655            writeln!(body, "    }}").ok();
1656            writeln!(body).ok();
1657        }
1658    }
1659
1660    // Expose convertWithVisitor in the public facade when visitor bridge is configured.
1661    if has_visitor_bridge {
1662        writeln!(body, "    /**").ok();
1663        writeln!(
1664            body,
1665            "     * Convert HTML to Markdown, invoking visitor callbacks during processing."
1666        )
1667        .ok();
1668        writeln!(body, "     */").ok();
1669        writeln!(
1670            body,
1671            "    public static ConversionResult convertWithVisitor(String html, ConversionOptions options, Visitor visitor)"
1672        )
1673        .ok();
1674        writeln!(body, "            throws {}Exception {{", raw_class).ok();
1675        writeln!(
1676            body,
1677            "        return {}.convertWithVisitor(html, options, visitor);",
1678            raw_class
1679        )
1680        .ok();
1681        writeln!(body, "    }}").ok();
1682        writeln!(body).ok();
1683    }
1684
1685    writeln!(body, "}}").ok();
1686
1687    // Now assemble the file with imports
1688    let mut out = String::with_capacity(body.len() + 512);
1689
1690    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1691    writeln!(out, "package {};", package).ok();
1692
1693    // Check what imports are needed based on content
1694    let has_list = body.contains("List<");
1695    let has_map = body.contains("Map<");
1696    let has_optional = body.contains("Optional<");
1697    let has_imports = has_list || has_map || has_optional;
1698
1699    if has_imports {
1700        writeln!(out).ok();
1701        if has_list {
1702            writeln!(out, "import java.util.List;").ok();
1703        }
1704        if has_map {
1705            writeln!(out, "import java.util.Map;").ok();
1706        }
1707        if has_optional {
1708            writeln!(out, "import java.util.Optional;").ok();
1709        }
1710    }
1711
1712    writeln!(out).ok();
1713    out.push_str(&body);
1714
1715    out
1716}
1717
1718// ---------------------------------------------------------------------------
1719// Opaque handle classes
1720// ---------------------------------------------------------------------------
1721
1722fn gen_opaque_handle_class(package: &str, typ: &TypeDef, prefix: &str) -> String {
1723    let mut out = String::with_capacity(1024);
1724    let class_name = &typ.name;
1725    let type_snake = class_name.to_snake_case();
1726
1727    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1728    writeln!(out, "package {};", package).ok();
1729    writeln!(out).ok();
1730    writeln!(out, "import java.lang.foreign.MemorySegment;").ok();
1731    writeln!(out).ok();
1732
1733    if !typ.doc.is_empty() {
1734        writeln!(out, "/**").ok();
1735        for line in typ.doc.lines() {
1736            writeln!(out, " * {}", line).ok();
1737        }
1738        writeln!(out, " */").ok();
1739    }
1740
1741    writeln!(out, "public class {} implements AutoCloseable {{", class_name).ok();
1742    writeln!(out, "    private final MemorySegment handle;").ok();
1743    writeln!(out).ok();
1744    writeln!(out, "    {}(MemorySegment handle) {{", class_name).ok();
1745    writeln!(out, "        this.handle = handle;").ok();
1746    writeln!(out, "    }}").ok();
1747    writeln!(out).ok();
1748    writeln!(out, "    MemorySegment handle() {{").ok();
1749    writeln!(out, "        return this.handle;").ok();
1750    writeln!(out, "    }}").ok();
1751    writeln!(out).ok();
1752    writeln!(out, "    @Override").ok();
1753    writeln!(out, "    public void close() {{").ok();
1754    writeln!(
1755        out,
1756        "        if (handle != null && !handle.equals(MemorySegment.NULL)) {{"
1757    )
1758    .ok();
1759    writeln!(out, "            try {{").ok();
1760    writeln!(
1761        out,
1762        "                NativeLib.{}_{}_FREE.invoke(handle);",
1763        prefix.to_uppercase(),
1764        type_snake.to_uppercase()
1765    )
1766    .ok();
1767    writeln!(out, "            }} catch (Throwable e) {{").ok();
1768    writeln!(
1769        out,
1770        "                throw new RuntimeException(\"Failed to free {}: \" + e.getMessage(), e);",
1771        class_name
1772    )
1773    .ok();
1774    writeln!(out, "            }}").ok();
1775    writeln!(out, "        }}").ok();
1776    writeln!(out, "    }}").ok();
1777    writeln!(out, "}}").ok();
1778
1779    out
1780}
1781
1782// ---------------------------------------------------------------------------
1783// Record types (Java records)
1784// ---------------------------------------------------------------------------
1785
1786/// Emit a Javadoc comment block into `out` at the given indentation level.
1787///
1788/// `indent` is the leading whitespace prepended to each line (e.g. `""` for
1789/// top-level declarations, `"    "` for class members).  Does nothing when
1790/// `doc` is empty.
1791fn emit_javadoc(out: &mut String, doc: &str, indent: &str) {
1792    if doc.is_empty() {
1793        return;
1794    }
1795    writeln!(out, "{indent}/**").ok();
1796    for line in doc.lines() {
1797        if line.is_empty() {
1798            writeln!(out, "{indent} *").ok();
1799        } else {
1800            let escaped = escape_javadoc_line(line);
1801            writeln!(out, "{indent} * {escaped}").ok();
1802        }
1803    }
1804    writeln!(out, "{indent} */").ok();
1805}
1806
1807/// Maximum line length before splitting record fields across multiple lines.
1808/// Checkstyle enforces 120 chars; we split at 100 to leave headroom for indentation.
1809const RECORD_LINE_WRAP_THRESHOLD: usize = 100;
1810
1811fn gen_record_type(package: &str, typ: &TypeDef, complex_enums: &AHashSet<String>, lang_rename_all: &str) -> String {
1812    // Generate the record body first, then scan for needed imports.
1813    // For each field, if the language uses camelCase but the JSON key is snake_case
1814    // (the Rust default), annotate with @JsonProperty so Jackson maps correctly.
1815    // Also collect per-field doc strings for Javadoc emission.
1816    let (field_list, field_docs): (Vec<String>, Vec<String>) = typ
1817        .fields
1818        .iter()
1819        .map(|f| {
1820            // Complex enums (tagged unions with data) can't be simple Java enums.
1821            // Use Object for flexible Jackson deserialization.
1822            let is_complex = matches!(&f.ty, TypeRef::Named(n) if complex_enums.contains(n.as_str()));
1823            let ftype = if is_complex {
1824                "Object".to_string()
1825            } else if f.optional {
1826                format!("Optional<{}>", java_boxed_type(&f.ty))
1827            } else {
1828                java_type(&f.ty).to_string()
1829            };
1830            let jname = safe_java_field_name(&f.name);
1831            // When the language convention is camelCase but the JSON wire format uses
1832            // snake_case (the Rust/serde default), add an explicit @JsonProperty annotation
1833            // so Jackson serialises/deserialises using the correct snake_case key.
1834            let decl = if lang_rename_all == "camelCase" && f.name.contains('_') {
1835                format!("@JsonProperty(\"{}\") {} {}", f.name, ftype, jname)
1836            } else {
1837                format!("{} {}", ftype, jname)
1838            };
1839            (decl, f.doc.clone())
1840        })
1841        .unzip();
1842
1843    // Build the single-line form to check length and scan for imports.
1844    // Doc strings are intentionally excluded from this check so the threshold
1845    // stays stable regardless of documentation presence.
1846    let single_line = format!("public record {}({}) {{ }}", typ.name, field_list.join(", "));
1847
1848    // Build the actual record declaration, splitting across lines if too long.
1849    let mut record_block = String::new();
1850    emit_javadoc(&mut record_block, &typ.doc, "");
1851    if single_line.len() > RECORD_LINE_WRAP_THRESHOLD && field_list.len() > 1 {
1852        writeln!(record_block, "public record {}(", typ.name).ok();
1853        for (i, (field, doc)) in field_list.iter().zip(field_docs.iter()).enumerate() {
1854            let comma = if i < field_list.len() - 1 { "," } else { "" };
1855            if !doc.is_empty() {
1856                // Inline single-line doc for record components in multi-line form.
1857                let doc_summary = escape_javadoc_line(doc.lines().next().unwrap_or("").trim());
1858                writeln!(record_block, "    /** {doc_summary} */").ok();
1859            }
1860            writeln!(record_block, "    {}{}", field, comma).ok();
1861        }
1862        writeln!(record_block, ") {{").ok();
1863    } else {
1864        writeln!(record_block, "public record {}({}) {{", typ.name, field_list.join(", ")).ok();
1865    }
1866
1867    // Add builder() factory method if type has defaults
1868    if typ.has_default {
1869        writeln!(record_block, "    public static {}Builder builder() {{", typ.name).ok();
1870        writeln!(record_block, "        return new {}Builder();", typ.name).ok();
1871        writeln!(record_block, "    }}").ok();
1872    }
1873
1874    writeln!(record_block, "}}").ok();
1875
1876    // Scan the single-line form to determine which imports are needed
1877    let needs_json_property = field_list.iter().any(|f| f.contains("@JsonProperty("));
1878    let mut out = String::with_capacity(record_block.len() + 512);
1879    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1880    writeln!(out, "package {};", package).ok();
1881    writeln!(out).ok();
1882    if single_line.contains("List<") {
1883        writeln!(out, "import java.util.List;").ok();
1884    }
1885    if single_line.contains("Map<") {
1886        writeln!(out, "import java.util.Map;").ok();
1887    }
1888    if single_line.contains("Optional<") {
1889        writeln!(out, "import java.util.Optional;").ok();
1890    }
1891    if needs_json_property {
1892        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
1893    }
1894    writeln!(out).ok();
1895    write!(out, "{}", record_block).ok();
1896
1897    out
1898}
1899
1900// ---------------------------------------------------------------------------
1901// Enum classes
1902// ---------------------------------------------------------------------------
1903
1904/// Apply a serde `rename_all` strategy to a variant name for Java codegen.
1905fn java_apply_rename_all(name: &str, rename_all: Option<&str>) -> String {
1906    match rename_all {
1907        Some("snake_case") => name.to_snake_case(),
1908        Some("camelCase") => name.to_lower_camel_case(),
1909        Some("PascalCase") => name.to_pascal_case(),
1910        Some("SCREAMING_SNAKE_CASE") => name.to_snake_case().to_uppercase(),
1911        Some("lowercase") => name.to_lowercase(),
1912        Some("UPPERCASE") => name.to_uppercase(),
1913        _ => name.to_lowercase(),
1914    }
1915}
1916
1917fn gen_enum_class(package: &str, enum_def: &EnumDef) -> String {
1918    let has_data_variants = enum_def.variants.iter().any(|v| !v.fields.is_empty());
1919
1920    // Tagged union: enum has a serde tag AND data variants → generate sealed interface hierarchy
1921    if enum_def.serde_tag.is_some() && has_data_variants {
1922        return gen_java_tagged_union(package, enum_def);
1923    }
1924
1925    let mut out = String::with_capacity(1024);
1926
1927    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
1928    writeln!(out, "package {};", package).ok();
1929    writeln!(out).ok();
1930    writeln!(out, "import com.fasterxml.jackson.annotation.JsonCreator;").ok();
1931    writeln!(out, "import com.fasterxml.jackson.annotation.JsonValue;").ok();
1932    writeln!(out).ok();
1933
1934    emit_javadoc(&mut out, &enum_def.doc, "");
1935    writeln!(out, "public enum {} {{", enum_def.name).ok();
1936
1937    for (i, variant) in enum_def.variants.iter().enumerate() {
1938        let comma = if i < enum_def.variants.len() - 1 { "," } else { ";" };
1939        // Use serde_rename if available, otherwise apply rename_all strategy
1940        let json_name = variant
1941            .serde_rename
1942            .clone()
1943            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
1944        if !variant.doc.is_empty() {
1945            let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
1946            writeln!(out, "    /** {doc_summary} */").ok();
1947        }
1948        writeln!(out, "    {}(\"{}\"){}", variant.name, json_name, comma).ok();
1949    }
1950
1951    writeln!(out).ok();
1952    writeln!(out, "    private final String value;").ok();
1953    writeln!(out).ok();
1954    writeln!(out, "    {}(String value) {{", enum_def.name).ok();
1955    writeln!(out, "        this.value = value;").ok();
1956    writeln!(out, "    }}").ok();
1957    writeln!(out).ok();
1958    writeln!(out, "    @JsonValue").ok();
1959    writeln!(out, "    public String getValue() {{").ok();
1960    writeln!(out, "        return value;").ok();
1961    writeln!(out, "    }}").ok();
1962    writeln!(out).ok();
1963    writeln!(out, "    @JsonCreator").ok();
1964    writeln!(out, "    public static {} fromValue(String value) {{", enum_def.name).ok();
1965    writeln!(out, "        for ({} e : values()) {{", enum_def.name).ok();
1966    writeln!(out, "            if (e.value.equalsIgnoreCase(value)) {{").ok();
1967    writeln!(out, "                return e;").ok();
1968    writeln!(out, "            }}").ok();
1969    writeln!(out, "        }}").ok();
1970    writeln!(
1971        out,
1972        "        throw new IllegalArgumentException(\"Unknown value: \" + value);"
1973    )
1974    .ok();
1975    writeln!(out, "    }}").ok();
1976
1977    writeln!(out, "}}").ok();
1978
1979    out
1980}
1981
1982/// Generate a Java sealed interface hierarchy for internally tagged enums.
1983///
1984/// Maps `#[serde(tag = "type_field", rename_all = "snake_case")]` Rust enums to
1985/// `@JsonTypeInfo` / `@JsonSubTypes` Java sealed interfaces with record implementations.
1986fn gen_java_tagged_union(package: &str, enum_def: &EnumDef) -> String {
1987    let tag_field = enum_def.serde_tag.as_deref().unwrap_or("type");
1988
1989    // Collect variant names to detect Java type name conflicts.
1990    // If a variant is named "List", "Map", or "Optional", using those type names
1991    // inside the sealed interface would refer to the nested record, not java.util.*.
1992    // We use fully qualified names in that case.
1993    let variant_names: std::collections::HashSet<&str> = enum_def.variants.iter().map(|v| v.name.as_str()).collect();
1994    let optional_type = if variant_names.contains("Optional") {
1995        "java.util.Optional"
1996    } else {
1997        "Optional"
1998    };
1999
2000    // @JsonProperty is only needed for variants with named (non-tuple) fields.
2001    let needs_json_property = enum_def
2002        .variants
2003        .iter()
2004        .any(|v| v.fields.iter().any(|f| !is_tuple_field_name(&f.name)));
2005
2006    let mut out = String::with_capacity(2048);
2007    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
2008    writeln!(out, "package {};", package).ok();
2009    writeln!(out).ok();
2010    if needs_json_property {
2011        writeln!(out, "import com.fasterxml.jackson.annotation.JsonProperty;").ok();
2012    }
2013    writeln!(out, "import com.fasterxml.jackson.annotation.JsonSubTypes;").ok();
2014    writeln!(out, "import com.fasterxml.jackson.annotation.JsonTypeInfo;").ok();
2015
2016    // Check if any field types need list/map/optional imports (only when not conflicting)
2017    let needs_list = !variant_names.contains("List")
2018        && enum_def
2019            .variants
2020            .iter()
2021            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Vec(_))));
2022    let needs_map = !variant_names.contains("Map")
2023        && enum_def
2024            .variants
2025            .iter()
2026            .any(|v| v.fields.iter().any(|f| matches!(&f.ty, TypeRef::Map(_, _))));
2027    let needs_optional =
2028        !variant_names.contains("Optional") && enum_def.variants.iter().any(|v| v.fields.iter().any(|f| f.optional));
2029    // Newtype/tuple variants (field name is a numeric index like "0") are flattened
2030    // into the parent JSON object using @JsonUnwrapped.
2031    let needs_unwrapped = enum_def
2032        .variants
2033        .iter()
2034        .any(|v| v.fields.len() == 1 && is_tuple_field_name(&v.fields[0].name));
2035    if needs_list {
2036        writeln!(out, "import java.util.List;").ok();
2037    }
2038    if needs_map {
2039        writeln!(out, "import java.util.Map;").ok();
2040    }
2041    if needs_optional {
2042        writeln!(out, "import java.util.Optional;").ok();
2043    }
2044    if needs_unwrapped {
2045        writeln!(out, "import com.fasterxml.jackson.annotation.JsonUnwrapped;").ok();
2046    }
2047    writeln!(out).ok();
2048
2049    emit_javadoc(&mut out, &enum_def.doc, "");
2050    // @JsonTypeInfo and @JsonSubTypes annotations
2051    writeln!(
2052        out,
2053        "@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = \"{tag_field}\", visible = false)"
2054    )
2055    .ok();
2056    writeln!(out, "@JsonSubTypes({{").ok();
2057    for (i, variant) in enum_def.variants.iter().enumerate() {
2058        let discriminator = variant
2059            .serde_rename
2060            .clone()
2061            .unwrap_or_else(|| java_apply_rename_all(&variant.name, enum_def.serde_rename_all.as_deref()));
2062        let comma = if i < enum_def.variants.len() - 1 { "," } else { "" };
2063        writeln!(
2064            out,
2065            "    @JsonSubTypes.Type(value = {}.{}.class, name = \"{}\"){}",
2066            enum_def.name, variant.name, discriminator, comma
2067        )
2068        .ok();
2069    }
2070    writeln!(out, "}})").ok();
2071    writeln!(out, "public sealed interface {} {{", enum_def.name).ok();
2072
2073    // Nested records for each variant
2074    for variant in &enum_def.variants {
2075        writeln!(out).ok();
2076        if variant.fields.is_empty() {
2077            // Unit variant
2078            if !variant.doc.is_empty() {
2079                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2080                writeln!(out, "    /** {doc_summary} */").ok();
2081            }
2082            writeln!(out, "    record {}() implements {} {{", variant.name, enum_def.name).ok();
2083            writeln!(out, "    }}").ok();
2084        } else {
2085            // Build field list using fully qualified names where variant names shadow imports
2086            let field_parts: Vec<String> = variant
2087                .fields
2088                .iter()
2089                .map(|f| {
2090                    let ftype = if f.optional {
2091                        let inner = java_boxed_type(&f.ty);
2092                        let inner_str = inner.as_ref();
2093                        // Replace "List"/"Map" with fully qualified if conflicting
2094                        let inner_qualified = if inner_str.starts_with("List<") && variant_names.contains("List") {
2095                            inner_str.replacen("List<", "java.util.List<", 1)
2096                        } else if inner_str.starts_with("Map<") && variant_names.contains("Map") {
2097                            inner_str.replacen("Map<", "java.util.Map<", 1)
2098                        } else {
2099                            inner_str.to_string()
2100                        };
2101                        format!("{optional_type}<{inner_qualified}>")
2102                    } else {
2103                        let t = java_type(&f.ty);
2104                        let t_str = t.as_ref();
2105                        if t_str.starts_with("List<") && variant_names.contains("List") {
2106                            t_str.replacen("List<", "java.util.List<", 1)
2107                        } else if t_str.starts_with("Map<") && variant_names.contains("Map") {
2108                            t_str.replacen("Map<", "java.util.Map<", 1)
2109                        } else {
2110                            t_str.to_string()
2111                        }
2112                    };
2113                    // Tuple/newtype variants have numeric field names (e.g. "0", "_0").
2114                    // These are not real JSON keys — serde flattens the inner type's fields
2115                    // alongside the tag. Use @JsonUnwrapped so Jackson does the same.
2116                    if is_tuple_field_name(&f.name) {
2117                        format!("@JsonUnwrapped {ftype} value")
2118                    } else {
2119                        let json_name = f.name.trim_start_matches('_');
2120                        let jname = safe_java_field_name(json_name);
2121                        format!("@JsonProperty(\"{json_name}\") {ftype} {jname}")
2122                    }
2123                })
2124                .collect();
2125
2126            let single = format!(
2127                "    record {}({}) implements {} {{ }}",
2128                variant.name,
2129                field_parts.join(", "),
2130                enum_def.name
2131            );
2132
2133            if !variant.doc.is_empty() {
2134                let doc_summary = escape_javadoc_line(variant.doc.lines().next().unwrap_or("").trim());
2135                writeln!(out, "    /** {doc_summary} */").ok();
2136            }
2137            if single.len() > RECORD_LINE_WRAP_THRESHOLD && field_parts.len() > 1 {
2138                writeln!(out, "    record {}(", variant.name).ok();
2139                for (i, fp) in field_parts.iter().enumerate() {
2140                    let comma = if i < field_parts.len() - 1 { "," } else { "" };
2141                    writeln!(out, "        {}{}", fp, comma).ok();
2142                }
2143                writeln!(out, "    ) implements {} {{", enum_def.name).ok();
2144                writeln!(out, "    }}").ok();
2145            } else {
2146                writeln!(
2147                    out,
2148                    "    record {}({}) implements {} {{ }}",
2149                    variant.name,
2150                    field_parts.join(", "),
2151                    enum_def.name
2152                )
2153                .ok();
2154            }
2155        }
2156    }
2157
2158    writeln!(out).ok();
2159    writeln!(out, "}}").ok();
2160    out
2161}
2162
2163// ---------------------------------------------------------------------------
2164// Helper functions for FFI marshalling
2165// ---------------------------------------------------------------------------
2166
2167fn gen_ffi_layout(ty: &TypeRef) -> String {
2168    match ty {
2169        TypeRef::Primitive(prim) => java_ffi_type(prim).to_string(),
2170        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "ValueLayout.ADDRESS".to_string(),
2171        TypeRef::Bytes => "ValueLayout.ADDRESS".to_string(),
2172        TypeRef::Optional(inner) => gen_ffi_layout(inner),
2173        TypeRef::Vec(_) => "ValueLayout.ADDRESS".to_string(),
2174        TypeRef::Map(_, _) => "ValueLayout.ADDRESS".to_string(),
2175        TypeRef::Named(_) => "ValueLayout.ADDRESS".to_string(),
2176        TypeRef::Unit => "".to_string(),
2177        TypeRef::Duration => "ValueLayout.JAVA_LONG".to_string(),
2178    }
2179}
2180
2181fn marshal_param_to_ffi(out: &mut String, name: &str, ty: &TypeRef, opaque_types: &AHashSet<String>, prefix: &str) {
2182    match ty {
2183        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2184            let cname = "c".to_string() + name;
2185            writeln!(out, "            var {} = arena.allocateFrom({});", cname, name).ok();
2186        }
2187        TypeRef::Named(type_name) => {
2188            let cname = "c".to_string() + name;
2189            if opaque_types.contains(type_name.as_str()) {
2190                // Opaque handles: pass the inner MemorySegment via .handle()
2191                writeln!(out, "            var {} = {}.handle();", cname, name).ok();
2192            } else {
2193                // Non-opaque named types: serialize to JSON, call _from_json to get FFI pointer.
2194                // The pointer must be freed after the FFI call with _free.
2195                let type_snake = type_name.to_snake_case();
2196                let from_json_handle = format!(
2197                    "NativeLib.{}_{}_FROM_JSON",
2198                    prefix.to_uppercase(),
2199                    type_snake.to_uppercase()
2200                );
2201                let _free_handle = format!("NativeLib.{}_{}_FREE", prefix.to_uppercase(), type_snake.to_uppercase());
2202                writeln!(
2203                    out,
2204                    "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2205                    cname, name, name
2206                )
2207                .ok();
2208                writeln!(
2209                    out,
2210                    "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;",
2211                    cname, cname, cname
2212                )
2213                .ok();
2214                writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2215                writeln!(
2216                    out,
2217                    "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2218                    from_json_handle, cname
2219                )
2220                .ok();
2221                writeln!(out, "                : MemorySegment.NULL;").ok();
2222            }
2223        }
2224        TypeRef::Optional(inner) => {
2225            // For optional types, marshal the inner type if not null
2226            match inner.as_ref() {
2227                TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => {
2228                    let cname = "c".to_string() + name;
2229                    writeln!(
2230                        out,
2231                        "            var {} = {} != null ? arena.allocateFrom({}) : MemorySegment.NULL;",
2232                        cname, name, name
2233                    )
2234                    .ok();
2235                }
2236                TypeRef::Named(type_name) => {
2237                    let cname = "c".to_string() + name;
2238                    if opaque_types.contains(type_name.as_str()) {
2239                        writeln!(
2240                            out,
2241                            "            var {} = {} != null ? {}.handle() : MemorySegment.NULL;",
2242                            cname, name, name
2243                        )
2244                        .ok();
2245                    } else {
2246                        // Non-opaque named type in Optional: serialize to JSON and call _from_json
2247                        let type_snake = type_name.to_snake_case();
2248                        let from_json_handle = format!(
2249                            "NativeLib.{}_{}_FROM_JSON",
2250                            prefix.to_uppercase(),
2251                            type_snake.to_uppercase()
2252                        );
2253                        writeln!(
2254                            out,
2255                            "            var {}Json = {} != null ? createObjectMapper().writeValueAsString({}) : null;",
2256                            cname, name, name
2257                        )
2258                        .ok();
2259                        writeln!(out, "            var {}JsonSeg = {}Json != null ? arena.allocateFrom({}Json) : MemorySegment.NULL;", cname, cname, cname).ok();
2260                        writeln!(out, "            var {} = {}Json != null", cname, cname).ok();
2261                        writeln!(
2262                            out,
2263                            "                ? (MemorySegment) {}.invoke({}JsonSeg)",
2264                            from_json_handle, cname
2265                        )
2266                        .ok();
2267                        writeln!(out, "                : MemorySegment.NULL;").ok();
2268                    }
2269                }
2270                _ => {
2271                    // Other optional types (primitives) pass through
2272                }
2273            }
2274        }
2275        TypeRef::Vec(_) | TypeRef::Map(_, _) => {
2276            // Vec/Map types: serialize to JSON string, then pass as a C string via arena.
2277            let cname = "c".to_string() + name;
2278            writeln!(
2279                out,
2280                "            var {}Json = createObjectMapper().writeValueAsString({});",
2281                cname, name
2282            )
2283            .ok();
2284            writeln!(out, "            var {} = arena.allocateFrom({}Json);", cname, cname).ok();
2285        }
2286        _ => {
2287            // Primitives and others pass through directly
2288        }
2289    }
2290}
2291
2292fn ffi_param_name(name: &str, ty: &TypeRef, _opaque_types: &AHashSet<String>) -> String {
2293    match ty {
2294        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => "c".to_string() + name,
2295        TypeRef::Named(_) => "c".to_string() + name,
2296        TypeRef::Vec(_) | TypeRef::Map(_, _) => "c".to_string() + name,
2297        TypeRef::Optional(inner) => match inner.as_ref() {
2298            TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json | TypeRef::Named(_) => {
2299                "c".to_string() + name
2300            }
2301            _ => name.to_string(),
2302        },
2303        _ => name.to_string(),
2304    }
2305}
2306
2307/// Build a `FunctionDescriptor` string for a given return layout and parameter layouts.
2308/// Handles void returns (ofVoid) and non-void returns (of) correctly.
2309fn gen_function_descriptor(return_layout: &str, param_layouts: &[String]) -> String {
2310    if return_layout.is_empty() {
2311        // Void return
2312        if param_layouts.is_empty() {
2313            "FunctionDescriptor.ofVoid()".to_string()
2314        } else {
2315            format!("FunctionDescriptor.ofVoid({})", param_layouts.join(", "))
2316        }
2317    } else {
2318        // Non-void return
2319        if param_layouts.is_empty() {
2320            format!("FunctionDescriptor.of({})", return_layout)
2321        } else {
2322            format!("FunctionDescriptor.of({}, {})", return_layout, param_layouts.join(", "))
2323        }
2324    }
2325}
2326
2327/// Returns true if the given return type maps to an FFI ADDRESS that represents a string
2328/// (i.e. the FFI returns `*mut c_char` which must be unmarshaled and freed).
2329fn is_ffi_string_return(ty: &TypeRef) -> bool {
2330    match ty {
2331        TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json => true,
2332        TypeRef::Optional(inner) => is_ffi_string_return(inner),
2333        _ => false,
2334    }
2335}
2336
2337/// Returns the appropriate Java cast type for non-string FFI return values.
2338fn java_ffi_return_cast(ty: &TypeRef) -> &'static str {
2339    match ty {
2340        TypeRef::Primitive(prim) => match prim {
2341            PrimitiveType::Bool => "boolean",
2342            PrimitiveType::U8 | PrimitiveType::I8 => "byte",
2343            PrimitiveType::U16 | PrimitiveType::I16 => "short",
2344            PrimitiveType::U32 | PrimitiveType::I32 => "int",
2345            PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "long",
2346            PrimitiveType::F32 => "float",
2347            PrimitiveType::F64 => "double",
2348        },
2349        TypeRef::Bytes | TypeRef::Vec(_) | TypeRef::Map(_, _) | TypeRef::Named(_) => "MemorySegment",
2350        _ => "MemorySegment",
2351    }
2352}
2353
2354fn gen_helper_methods(out: &mut String, prefix: &str, class_name: &str) {
2355    // Only emit helper methods that are actually called in the generated body.
2356    let needs_check_last_error = out.contains("checkLastError()");
2357    let needs_read_cstring = out.contains("readCString(");
2358    let needs_read_bytes = out.contains("readBytes(");
2359    let needs_create_object_mapper = out.contains("createObjectMapper()");
2360
2361    if !needs_check_last_error && !needs_read_cstring && !needs_read_bytes && !needs_create_object_mapper {
2362        return;
2363    }
2364
2365    writeln!(out, "    // Helper methods for FFI marshalling").ok();
2366    writeln!(out).ok();
2367
2368    if needs_check_last_error {
2369        // Reads the last FFI error code and, if non-zero, reads the error message and throws.
2370        // Called immediately after a null-pointer return from an FFI call.
2371        writeln!(out, "    private static void checkLastError() throws Throwable {{").ok();
2372        writeln!(
2373            out,
2374            "        int errCode = (int) NativeLib.{}_LAST_ERROR_CODE.invoke();",
2375            prefix.to_uppercase()
2376        )
2377        .ok();
2378        writeln!(out, "        if (errCode != 0) {{").ok();
2379        writeln!(
2380            out,
2381            "            var ctxPtr = (MemorySegment) NativeLib.{}_LAST_ERROR_CONTEXT.invoke();",
2382            prefix.to_uppercase()
2383        )
2384        .ok();
2385        writeln!(
2386            out,
2387            "            String msg = ctxPtr.reinterpret(Long.MAX_VALUE).getString(0);"
2388        )
2389        .ok();
2390        writeln!(out, "            throw new {}Exception(errCode, msg);", class_name).ok();
2391        writeln!(out, "        }}").ok();
2392        writeln!(out, "    }}").ok();
2393        writeln!(out).ok();
2394    }
2395
2396    if needs_create_object_mapper {
2397        // Emit a configured ObjectMapper factory:
2398        //   - findAndRegisterModules() to pick up jackson-datatype-jdk8 (Optional support)
2399        //   - ACCEPT_CASE_INSENSITIVE_ENUMS so enum names like "json_ld" match JsonLd, etc.
2400        // Field name mapping relies on explicit @JsonProperty annotations on record components
2401        // (generated by alef for snake_case FFI fields on camelCase Java records).
2402        writeln!(
2403            out,
2404            "    private static com.fasterxml.jackson.databind.ObjectMapper createObjectMapper() {{"
2405        )
2406        .ok();
2407        writeln!(out, "        return new com.fasterxml.jackson.databind.ObjectMapper()").ok();
2408        writeln!(
2409            out,
2410            "            .registerModule(new com.fasterxml.jackson.datatype.jdk8.Jdk8Module())"
2411        )
2412        .ok();
2413        writeln!(out, "            .findAndRegisterModules()").ok();
2414        writeln!(
2415            out,
2416            "            .setSerializationInclusion(com.fasterxml.jackson.annotation.JsonInclude.Include.NON_NULL)"
2417        )
2418        .ok();
2419        writeln!(
2420            out,
2421            "            .configure(com.fasterxml.jackson.databind.MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS, true);"
2422        )
2423        .ok();
2424        writeln!(out, "    }}").ok();
2425        writeln!(out).ok();
2426    }
2427
2428    if needs_read_cstring {
2429        writeln!(out, "    private static String readCString(MemorySegment ptr) {{").ok();
2430        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2431        writeln!(out, "            return null;").ok();
2432        writeln!(out, "        }}").ok();
2433        writeln!(out, "        return ptr.getUtf8String(0);").ok();
2434        writeln!(out, "    }}").ok();
2435        writeln!(out).ok();
2436    }
2437
2438    if needs_read_bytes {
2439        writeln!(
2440            out,
2441            "    private static byte[] readBytes(MemorySegment ptr, long len) {{"
2442        )
2443        .ok();
2444        writeln!(out, "        if (ptr == null || ptr.address() == 0) {{").ok();
2445        writeln!(out, "            return new byte[0];").ok();
2446        writeln!(out, "        }}").ok();
2447        writeln!(out, "        byte[] bytes = new byte[(int) len];").ok();
2448        writeln!(
2449            out,
2450            "        MemorySegment.copy(ptr, ValueLayout.JAVA_BYTE.byteSize() * 0, bytes, 0, (int) len);"
2451        )
2452        .ok();
2453        writeln!(out, "        return bytes;").ok();
2454        writeln!(out, "    }}").ok();
2455    }
2456}
2457
2458// ---------------------------------------------------------------------------
2459// Builder class for types with defaults
2460// ---------------------------------------------------------------------------
2461
2462/// Format a default value for an Optional field, wrapping it in Optional.of()
2463/// with proper Java literal syntax.
2464fn format_optional_value(ty: &TypeRef, default: &str) -> String {
2465    // Check if the default is already wrapped (e.g., "Optional.of(...)" or "Optional.empty()")
2466    if default.contains("Optional.") {
2467        return default.to_string();
2468    }
2469
2470    // Unwrap Optional types to get the inner type
2471    let inner_ty = match ty {
2472        TypeRef::Optional(inner) => inner.as_ref(),
2473        other => other,
2474    };
2475
2476    // Determine the proper literal suffix based on type
2477    let formatted_value = match inner_ty {
2478        TypeRef::Primitive(p) => match p {
2479            PrimitiveType::I64 | PrimitiveType::U64 | PrimitiveType::Isize | PrimitiveType::Usize => {
2480                // Add 'L' suffix for long values if not already present
2481                if default.ends_with('L') || default.ends_with('l') {
2482                    default.to_string()
2483                } else if default.parse::<i64>().is_ok() {
2484                    format!("{}L", default)
2485                } else {
2486                    default.to_string()
2487                }
2488            }
2489            PrimitiveType::F32 => {
2490                // Add 'f' suffix for float values if not already present
2491                if default.ends_with('f') || default.ends_with('F') {
2492                    default.to_string()
2493                } else if default.parse::<f32>().is_ok() {
2494                    format!("{}f", default)
2495                } else {
2496                    default.to_string()
2497                }
2498            }
2499            PrimitiveType::F64 => {
2500                // Double defaults can have optional 'd' suffix, but 0.0 is fine
2501                default.to_string()
2502            }
2503            _ => default.to_string(),
2504        },
2505        _ => default.to_string(),
2506    };
2507
2508    format!("Optional.of({})", formatted_value)
2509}
2510
2511fn gen_builder_class(package: &str, typ: &TypeDef) -> String {
2512    let mut body = String::with_capacity(2048);
2513
2514    emit_javadoc(&mut body, &typ.doc, "");
2515    writeln!(body, "public class {}Builder {{", typ.name).ok();
2516    writeln!(body).ok();
2517
2518    // Generate field declarations with defaults
2519    for field in &typ.fields {
2520        let field_name = safe_java_field_name(&field.name);
2521
2522        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2523        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2524            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2525        {
2526            continue;
2527        }
2528
2529        // Duration maps to primitive `long` in the public record, but in builder
2530        // classes we use boxed `Long` so that `null` can represent "not set".
2531        let field_type = if field.optional {
2532            format!("Optional<{}>", java_boxed_type(&field.ty))
2533        } else if matches!(field.ty, TypeRef::Duration) {
2534            java_boxed_type(&field.ty).to_string()
2535        } else {
2536            java_type(&field.ty).to_string()
2537        };
2538
2539        let default_value = if field.optional {
2540            // For Optional fields, always use Optional.empty() or Optional.of(value)
2541            if let Some(default) = &field.default {
2542                // If there's an explicit default, wrap it in Optional.of()
2543                format_optional_value(&field.ty, default)
2544            } else {
2545                // If no default, use Optional.empty()
2546                "Optional.empty()".to_string()
2547            }
2548        } else {
2549            // For non-Optional fields, use regular defaults
2550            if let Some(default) = &field.default {
2551                default.clone()
2552            } else {
2553                match &field.ty {
2554                    TypeRef::String | TypeRef::Char | TypeRef::Path => "\"\"".to_string(),
2555                    TypeRef::Json => "null".to_string(),
2556                    TypeRef::Bytes => "new byte[0]".to_string(),
2557                    TypeRef::Primitive(p) => match p {
2558                        PrimitiveType::Bool => "false".to_string(),
2559                        PrimitiveType::F32 | PrimitiveType::F64 => "0.0".to_string(),
2560                        _ => "0".to_string(),
2561                    },
2562                    TypeRef::Vec(_) => "List.of()".to_string(),
2563                    TypeRef::Map(_, _) => "Map.of()".to_string(),
2564                    TypeRef::Optional(_) => "Optional.empty()".to_string(),
2565                    TypeRef::Duration => "null".to_string(),
2566                    _ => "null".to_string(),
2567                }
2568            }
2569        };
2570
2571        writeln!(body, "    private {} {} = {};", field_type, field_name, default_value).ok();
2572    }
2573
2574    writeln!(body).ok();
2575
2576    // Generate withXxx() methods
2577    for field in &typ.fields {
2578        // Skip unnamed tuple fields (name is "_0", "_1", "0", "1", etc.) — Java requires named fields
2579        if field.name.starts_with('_') && field.name[1..].chars().all(|c| c.is_ascii_digit())
2580            || field.name.chars().next().is_none_or(|c| c.is_ascii_digit())
2581        {
2582            continue;
2583        }
2584
2585        let field_name = safe_java_field_name(&field.name);
2586        let field_name_pascal = to_class_name(&field.name);
2587        let field_type = if field.optional {
2588            format!("Optional<{}>", java_boxed_type(&field.ty))
2589        } else if matches!(field.ty, TypeRef::Duration) {
2590            java_boxed_type(&field.ty).to_string()
2591        } else {
2592            java_type(&field.ty).to_string()
2593        };
2594
2595        writeln!(
2596            body,
2597            "    public {}Builder with{}({} value) {{",
2598            typ.name, field_name_pascal, field_type
2599        )
2600        .ok();
2601        writeln!(body, "        this.{} = value;", field_name).ok();
2602        writeln!(body, "        return this;").ok();
2603        writeln!(body, "    }}").ok();
2604        writeln!(body).ok();
2605    }
2606
2607    // Generate build() method
2608    writeln!(body, "    public {} build() {{", typ.name).ok();
2609    writeln!(body, "        return new {}(", typ.name).ok();
2610    let non_tuple_fields: Vec<_> = typ
2611        .fields
2612        .iter()
2613        .filter(|f| {
2614            // Include named fields (skip unnamed tuple fields)
2615            !(f.name.starts_with('_') && f.name[1..].chars().all(|c| c.is_ascii_digit())
2616                || f.name.chars().next().is_none_or(|c| c.is_ascii_digit()))
2617        })
2618        .collect();
2619    for (i, field) in non_tuple_fields.iter().enumerate() {
2620        let field_name = safe_java_field_name(&field.name);
2621        let comma = if i < non_tuple_fields.len() - 1 { "," } else { "" };
2622        writeln!(body, "            {}{}", field_name, comma).ok();
2623    }
2624    writeln!(body, "        );").ok();
2625    writeln!(body, "    }}").ok();
2626
2627    writeln!(body, "}}").ok();
2628
2629    // Now assemble with conditional imports based on what's actually used in the body
2630    let mut out = String::with_capacity(body.len() + 512);
2631
2632    writeln!(out, "// DO NOT EDIT - auto-generated by alef").ok();
2633    writeln!(out, "package {};", package).ok();
2634    writeln!(out).ok();
2635
2636    if body.contains("List<") {
2637        writeln!(out, "import java.util.List;").ok();
2638    }
2639    if body.contains("Map<") {
2640        writeln!(out, "import java.util.Map;").ok();
2641    }
2642    if body.contains("Optional<") {
2643        writeln!(out, "import java.util.Optional;").ok();
2644    }
2645
2646    writeln!(out).ok();
2647    out.push_str(&body);
2648
2649    out
2650}