//! HarmonyOS ArkTS shim emitter extracted from `compile.rs`.
//!
//! Writes the ArkTS files that wrap the Perry-emitted `.so` for HarmonyOS
//! targets — see `emit_harmonyos_arkts_stubs` for the file-by-file contract.
//! All HAP-shape details (NAPI export typing, ArkUI page registration, callback
//! bridge declarations) live here so the orchestrator just calls one entry
//! point per HarmonyOS build.

use anyhow::Result;
use std::fs;
use std::path::Path;

/// Emit the ArkTS shim next to the compiled `.so` for HarmonyOS targets.
///
/// Writes two files:
///
/// * `ets/entryability/EntryAbility.ets` — UIAbility subclass that runs
///   the Perry TS entry once in `onCreate`. Further ArkTS lifecycle hooks
///   (background, destroy) are left as no-ops; a future version can
///   forward them into TS callbacks.
///
/// * `ets/pages/Index.ets` — the landing page referenced by
///   `windowStage.loadContent`. Minimal: a single centered `Text` that
///   confirms the TS code ran. The HAP bundler (PR B.3) will be the first
///   consumer of this file; for now we emit a valid ArkUI component so
///   DevEco Studio or `hvigor check` won't reject the shim.
///
/// `so_filename` is templated into Index.ets's `import` so the `dlopen`
/// name matches whatever the user passed to `-o` (defaults to
/// `lib<stem>.so`).
pub(super) fn emit_harmonyos_arkts_stubs(
    output_dir: &Path,
    so_filename: &str,
    index_ets: Option<&str>,
) -> Result<()> {
    let ets_dir = output_dir.join("ets");
    let entryability_dir = ets_dir.join("entryability");
    fs::create_dir_all(&entryability_dir)?;

    // Two paths, same EntryAbility shape:
    //
    // 1. UI program (Phase 2): the user's TS calls `App({body: ...})`.
    //    `perry-codegen-arkts` walked the HIR and emitted a real ArkUI
    //    `pages/Index.ets`. Restore `onWindowStageCreate` so the ability
    //    actually loads the page after `perryEntry.run()` returns.
    //
    // 2. Logic-only program (Phase 1, original): the .ets in the HAP is
    //    just the EntryAbility wrapper that runs Perry's compiled main()
    //    once per ability launch. Output reaches hilog. Window stays
    //    blank because there's no page to load — calling
    //    `windowStage.loadContent('pages/Index')` without an Index.ets
    //    would crash at runtime, so we omit it.
    //
    // `es2abc --extension ts` accepts plain-TypeScript in either case;
    // ArkUI decorators (`@Entry @Component struct`) live in the
    // emitted-Index.ets only, never in EntryAbility.
    let (window_imports, window_hooks) = if index_ets.is_some() {
        (
            "import window from '@ohos.window';\n",
            "\x20\x20\x20\x20onWindowStageCreate(windowStage: window.WindowStage) {\n\
             \x20\x20\x20\x20\x20\x20\x20\x20windowStage.loadContent('pages/Index');\n\
             \x20\x20\x20\x20}\n",
        )
    } else {
        ("", "")
    };
    let entry_ability = format!(
        "// Auto-generated by Perry — do not edit.\n\
         // Regenerated every `perry compile --target harmonyos`.\n\
         import UIAbility from '@ohos.app.ability.UIAbility';\n\
         {window_imports}\
         import perryEntry from '{so}';\n\
         \n\
         export default class EntryAbility extends UIAbility {{\n\
         \x20\x20\x20\x20onCreate(want, launchParam) {{\n\
         \x20\x20\x20\x20\x20\x20\x20\x20// Run the compiled Perry program once per ability instance.\n\
         \x20\x20\x20\x20\x20\x20\x20\x20// Returns the process-style exit code; ignored here.\n\
         \x20\x20\x20\x20\x20\x20\x20\x20perryEntry.run();\n\
         \x20\x20\x20\x20}}\n\
         \x20\x20\x20\x20onDestroy() {{}}\n\
         {window_hooks}\
         \x20\x20\x20\x20onForeground() {{}}\n\
         \x20\x20\x20\x20onBackground() {{}}\n\
         }}\n",
        window_imports = window_imports,
        window_hooks = window_hooks,
        so = so_filename
    );
    fs::write(entryability_dir.join("EntryAbility.ets"), entry_ability)?;

    if let Some(page_src) = index_ets {
        let pages_dir = ets_dir.join("pages");
        fs::create_dir_all(&pages_dir)?;
        fs::write(pages_dir.join("Index.ets"), page_src)?;
    }

    // Phase 2 v2: NAPI-typed declaration for the .so's exports, so DevEco's
    // ArkTS compiler can type-check `perryEntry.invokeCallback(0)` calls
    // emitted in the auto-generated Index.ets. Without this, hvigor fails
    // CompileArkTS with `Property 'invokeCallback' does not exist on type
    // typeof import("…/libentry/Index")` even though the runtime export is
    // present in the .so. DevEco scaffolds an initial .d.ts when the user
    // first creates a NAPI module via the New > C++ wizard, but it doesn't
    // refresh on subsequent builds — Perry owning the regeneration here
    // ensures every build's .d.ts matches the actual exports in `ohos_napi`.
    //
    // Contributors: when adding a new NAPI export to ohos_napi.rs, add the
    // matching declaration here. Otherwise existing user projects will see
    // a typecheck failure even after they splice the new .so. (We don't
    // know the user's target project layout, so we always emit to a
    // sibling `cpp/types/libentry/Index.d.ts` next to the .ets — DevEco
    // resolves NAPI types from this path by the v0.5.398-era hvigor.)
    let dts_dir = output_dir.join("cpp/types/libentry");
    fs::create_dir_all(&dts_dir)?;
    let dts_source = "// Auto-generated by Perry. Matches the NAPI exports registered by\n\
                      // libentry.so's .init_array constructor (crates/perry-runtime/src/ohos_napi.rs).\n\
                      export const run: () => number;\n\
                      // Phase 2 v2: ArkUI .onClick / event handlers call this to invoke a\n\
                      // Perry TS closure registered at startup by perry-codegen-arkts.\n\
                      export const invokeCallback: (idx: number) => void;\n\
                      // Phase 2 v3 Option 1: pop one queued toast message produced by\n\
                      // `showToast(...)` inside a closure body, or undefined when the\n\
                      // queue is empty. The auto-emitted onClick body loops on this\n\
                      // and dispatches each entry to `promptAction.showToast`.\n\
                      export const drainToast: () => string | undefined;\n\
                      // Phase 2 v3 Option 2: pop one queued (id, value) text update\n\
                      // produced by `setText(...)` inside a closure body, or undefined\n\
                      // when the queue is empty. The auto-emitted onClick body loops\n\
                      // and applies each entry to the matching `@State text_<id>` via\n\
                      // a switch dispatch generated by perry-codegen-arkts.\n\
                      export const drainTextUpdate: () => { id: string; value: string } | undefined;\n\
                      // Phase 2 v2.5: dispatch a registered closure with one value arg.\n\
                      // ArkUI's Toggle/TextField/Slider onChange handlers call this with\n\
                      // the event payload — boolean for Toggle, string for TextField,\n\
                      // number for Slider. NAPI handler dispatches by typeof and routes\n\
                      // through js_closure_call1 with the marshaled NaN-boxed value.\n\
                      export const invokeCallback1: (idx: number, value: boolean | string | number) => void;\n\
                      // Phase 2 v3.5: pop one queued (id, hidden) visibility update\n\
                      // produced by `widgetSetHidden(...)` inside a closure body, or\n\
                      // undefined when the queue is empty. The auto-emitted onClick\n\
                      // body loops and applies each entry to the matching\n\
                      // `@State hidden_<id>: boolean` via a switch dispatch generated\n\
                      // by perry-codegen-arkts.\n\
                      export const drainVisibilityUpdate: () => { id: string; hidden: boolean } | undefined;\n\
                      // Phase 2 v3.6: pop one queued (target_synth, view_id) content-view\n\
                      // update produced by a view-builder function call inside a closure\n\
                      // body, or undefined when the queue is empty. The auto-emitted\n\
                      // onClick body loops and applies each entry to the matching\n\
                      // `@State contentView_<target_synth>: string` via a switch\n\
                      // dispatch generated by perry-codegen-arkts.\n\
                      export const drainContentViewUpdate: () => { id: string; view: string } | undefined;\n";
    fs::write(dts_dir.join("Index.d.ts"), dts_source)?;

    Ok(())
}
