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