const std = @import("std");
const Allocator = std.mem.Allocator;
const interp_mod = @import("interp.zig");
const Value = interp_mod.Value;
const Interpreter = interp_mod.Interpreter;
const inst = @import("inst.zig");
const FuncId = inst.FuncId;

// ── BuildConfig ─────────────────────────────────────────────────────────
// Mutable build configuration accumulated by #run blocks via #compiler methods.

/// `(src_dir, dest_in_bundle)` pair recorded by
/// `BuildOptions.add_asset_dir(src, dest)`. The sx bundler walks the
/// list and recursively copies each `src` directory into the bundle
/// at the relative `dest` path (e.g. `("assets", "assets")` copies
/// `./assets/` to `<bundle>/assets/`). Android's Week-7 APK path will
/// zip the same pairs into the unaligned APK.
pub const AssetDir = struct {
    src: []const u8,
    dest: []const u8,
};

pub const BuildConfig = struct {
    link_flags: std.ArrayList([]const u8) = .empty,
    frameworks: std.ArrayList([]const u8) = .empty,
    asset_dirs: std.ArrayList(AssetDir) = .empty,
    output_path: ?[]const u8 = null,
    wasm_shell_path: ?[]const u8 = null,

    /// Post-link callback registered via
    /// `BuildOptions.set_post_link_callback(fn)`. When set, the
    /// compiler re-enters the IR interpreter after `target.link()`
    /// and invokes this function with no args. A `false` return is
    /// treated as a build failure.
    post_link_callback_fn: ?FuncId = null,
    /// Alternative to `post_link_callback_fn`: the qualified name of
    /// a module whose `bundle_main` function should be called
    /// post-link.
    post_link_module: ?[]const u8 = null,

    /// Path of the freshly-linked binary, populated by `main.zig`
    /// right before the post-link callback runs. The sx-side bundler
    /// reads this via `binary_path()` to know what file to wrap.
    binary_path: ?[]const u8 = null,

    // Apple `.app` / Android `.apk` bundling parameters. Set either
    // by the sx-side `BuildOptions.set_bundle_*` methods (preferred)
    // or by main.zig from CLI flags (transitional fallback). The sx
    // bundler reads them via the matching accessor methods.
    bundle_path: ?[]const u8 = null,
    bundle_id: ?[]const u8 = null,
    codesign_identity: ?[]const u8 = null,
    provisioning_profile: ?[]const u8 = null,

    /// Target triple as supplied to `--target` (canonicalized).
    /// Populated by main.zig before the post-link callback runs so the
    /// sx bundler can switch on iOS vs. macOS vs. simulator.
    target_triple: ?[]const u8 = null,

    /// Frameworks the binary links against (`-framework` names) and
    /// the search paths to look them up in (`-F` directories), forwarded
    /// from the link step so the sx bundler can embed them into
    /// `<bundle>/Frameworks/`.
    target_frameworks: []const []const u8 = &.{},
    target_framework_paths: []const []const u8 = &.{},

    /// User-supplied `AndroidManifest.xml` override (`--manifest <path>`
    /// or `BuildOptions.set_manifest_path("...")`). When null, the
    /// Android bundler synthesizes a default manifest.
    manifest_path: ?[]const u8 = null,
    /// User-supplied debug keystore path (`--keystore <path>` or
    /// `BuildOptions.set_keystore_path("...")`). When null, the Android
    /// bundler uses `$HOME/.android/debug.keystore` (auto-generated on
    /// first use via `keytool`).
    keystore_path: ?[]const u8 = null,

    /// `#jni_main #jni_class("path") { ... }` decls discovered during
    /// lowering, paired with their pre-rendered Java source. The
    /// Android bundler writes each entry to
    /// `<stage>/java/<pkg>/<Class>.java`, compiles via `javac` + `d8`,
    /// and bundles the resulting `classes.dex` into the APK. Slices
    /// reference compiler-owned memory that outlives the post-link
    /// callback.
    jni_main_foreign_paths: []const []const u8 = &.{},
    jni_main_java_sources: []const []const u8 = &.{},

    pub fn deinit(self: *BuildConfig, alloc: Allocator) void {
        self.link_flags.deinit(alloc);
        self.frameworks.deinit(alloc);
        self.asset_dirs.deinit(alloc);
    }
};

// ── Hook system ─────────────────────────────────────────────────────────

pub const HookError = error{
    CannotEvalComptime,
    TypeError,
};

/// Hook function signature. Receives the interpreter (for heap/string access),
/// resolved argument values, and the mutable build config.
pub const HookFn = *const fn (
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value;

pub const Registry = struct {
    hooks: std.StringHashMap(HookFn),

    pub fn init(alloc: Allocator) Registry {
        return .{ .hooks = std.StringHashMap(HookFn).init(alloc) };
    }

    pub fn deinit(self: *Registry) void {
        self.hooks.deinit();
    }

    pub fn get(self: *const Registry, name: []const u8) ?HookFn {
        return self.hooks.get(name);
    }

    /// Register all built-in compiler hooks.
    pub fn registerDefaults(self: *Registry) void {
        self.hooks.put("build_options", &hookBuildOptions) catch {};
        self.hooks.put("BuildOptions.add_link_flag", &hookAddLinkFlag) catch {};
        self.hooks.put("BuildOptions.add_framework", &hookAddFramework) catch {};
        self.hooks.put("BuildOptions.add_asset_dir", &hookAddAssetDir) catch {};
        self.hooks.put("BuildOptions.asset_dir_count", &hookAssetDirCount) catch {};
        self.hooks.put("BuildOptions.asset_dir_src_at", &hookAssetDirSrcAt) catch {};
        self.hooks.put("BuildOptions.asset_dir_dest_at", &hookAssetDirDestAt) catch {};
        self.hooks.put("BuildOptions.set_output_path", &hookSetOutputPath) catch {};
        self.hooks.put("BuildOptions.set_wasm_shell", &hookSetWasmShell) catch {};
        self.hooks.put("BuildOptions.set_post_link_callback", &hookSetPostLinkCallback) catch {};
        self.hooks.put("BuildOptions.set_post_link_module", &hookSetPostLinkModule) catch {};
        self.hooks.put("BuildOptions.binary_path", &hookGetBinaryPath) catch {};
        // Bundling setters
        self.hooks.put("BuildOptions.set_bundle_path", &hookSetBundlePath) catch {};
        self.hooks.put("BuildOptions.set_bundle_id", &hookSetBundleId) catch {};
        self.hooks.put("BuildOptions.set_codesign_identity", &hookSetCodesignIdentity) catch {};
        self.hooks.put("BuildOptions.set_provisioning_profile", &hookSetProvisioningProfile) catch {};
        // Bundling accessors
        self.hooks.put("BuildOptions.bundle_path", &hookGetBundlePath) catch {};
        self.hooks.put("BuildOptions.bundle_id", &hookGetBundleId) catch {};
        self.hooks.put("BuildOptions.codesign_identity", &hookGetCodesignIdentity) catch {};
        self.hooks.put("BuildOptions.provisioning_profile", &hookGetProvisioningProfile) catch {};
        // Target accessors — mirror TargetConfig.is{MacOS,IOS,IOSDevice,IOSSimulator,Android}()
        self.hooks.put("BuildOptions.target_triple", &hookGetTargetTriple) catch {};
        self.hooks.put("BuildOptions.is_macos", &hookIsMacOS) catch {};
        self.hooks.put("BuildOptions.is_ios", &hookIsIOS) catch {};
        self.hooks.put("BuildOptions.is_ios_device", &hookIsIOSDevice) catch {};
        self.hooks.put("BuildOptions.is_ios_simulator", &hookIsIOSSimulator) catch {};
        self.hooks.put("BuildOptions.is_android", &hookIsAndroid) catch {};
        // Android-specific setters + accessors
        self.hooks.put("BuildOptions.set_manifest_path", &hookSetManifestPath) catch {};
        self.hooks.put("BuildOptions.manifest_path", &hookGetManifestPath) catch {};
        self.hooks.put("BuildOptions.set_keystore_path", &hookSetKeystorePath) catch {};
        self.hooks.put("BuildOptions.keystore_path", &hookGetKeystorePath) catch {};
        // #jni_main class emissions, exposed by index so bundle.sx can iterate.
        self.hooks.put("BuildOptions.jni_main_count", &hookJniMainCount) catch {};
        self.hooks.put("BuildOptions.jni_main_foreign_path_at", &hookJniMainForeignPathAt) catch {};
        self.hooks.put("BuildOptions.jni_main_java_source_at", &hookJniMainJavaSourceAt) catch {};
        // Framework list accessors (for `.app/Frameworks/` embedding)
        self.hooks.put("BuildOptions.framework_count", &hookFrameworkCount) catch {};
        self.hooks.put("BuildOptions.framework_at", &hookFrameworkAt) catch {};
        self.hooks.put("BuildOptions.framework_path_count", &hookFrameworkPathCount) catch {};
        self.hooks.put("BuildOptions.framework_path_at", &hookFrameworkPathAt) catch {};
    }
};

// ── build_options() hook ────────────────────────────────────────────────

fn hookBuildOptions(
    _: *const Interpreter,
    _: []const Value,
    _: *BuildConfig,
    _: Allocator,
) HookError!Value {
    // build_options() returns a sentinel value; the real work happens
    // when methods like add_link_flag/set_output_path are called on it.
    return .void_val;
}

// ── BuildOptions hooks ──────────────────────────────────────────────────

fn hookAddLinkFlag(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), flag_string]
    if (args.len < 2) return .void_val;
    const str_val = args[1];
    if (str_val.asString(interp)) |s| {
        bc.link_flags.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookAddFramework(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), framework_name]
    if (args.len < 2) return .void_val;
    const str_val = args[1];
    if (str_val.asString(interp)) |s| {
        bc.frameworks.append(alloc, alloc.dupe(u8, s) catch return error.CannotEvalComptime) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookAddAssetDir(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), src_path, dest_path_in_bundle]
    if (args.len < 3) return .void_val;
    const src = args[1].asString(interp) orelse return error.TypeError;
    const dest = args[2].asString(interp) orelse return error.TypeError;
    const src_dup = alloc.dupe(u8, src) catch return error.CannotEvalComptime;
    const dest_dup = alloc.dupe(u8, dest) catch return error.CannotEvalComptime;
    bc.asset_dirs.append(alloc, .{ .src = src_dup, .dest = dest_dup }) catch return error.CannotEvalComptime;
    return .void_val;
}

fn hookAssetDirCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .int = @intCast(bc.asset_dirs.items.len) };
}

fn hookAssetDirSrcAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx = args[1].asInt() orelse return error.TypeError;
    if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" };
    return Value{ .string = bc.asset_dirs.items[@intCast(idx)].src };
}

fn hookAssetDirDestAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx = args[1].asInt() orelse return error.TypeError;
    if (idx < 0 or @as(usize, @intCast(idx)) >= bc.asset_dirs.items.len) return Value{ .string = "" };
    return Value{ .string = bc.asset_dirs.items[@intCast(idx)].dest };
}

fn hookSetOutputPath(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), path_string]
    if (args.len < 2) return .void_val;
    const str_val = args[1];
    if (str_val.asString(interp)) |s| {
        bc.output_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookSetWasmShell(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), path_string]
    if (args.len < 2) return .void_val;
    const str_val = args[1];
    if (str_val.asString(interp)) |s| {
        bc.wasm_shell_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookSetPostLinkCallback(
    _: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    _: Allocator,
) HookError!Value {
    // args: [self (BuildOptions value), fn_value]. We accept a function
    // value (.func_ref) and stash the FuncId so `main.zig` can re-enter
    // the interpreter after linking.
    if (args.len < 2) return .void_val;
    switch (args[1]) {
        .func_ref => |id| bc.post_link_callback_fn = id,
        else => return error.TypeError,
    }
    return .void_val;
}

fn hookSetPostLinkModule(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.post_link_module = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

/// Read the linked-binary path that main.zig populated right before
/// invoking the post-link callback. Returns the fat-string aggregate
/// the interpreter normally hands out for sx `string` values.
fn hookGetBinaryPath(
    interp: *const Interpreter,
    _: []const Value,
    bc: *BuildConfig,
    _: Allocator,
) HookError!Value {
    _ = interp;
    const path = bc.binary_path orelse "";
    return Value{ .string = path };
}

// ── Bundling setters & accessors ─────────────────────────────────────
// Same pattern as set_output_path: take a string arg, dupe into the
// long-lived allocator, store on BuildConfig. The companion accessor
// reads back the same field; empty string when unset.

fn hookSetBundlePath(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.bundle_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookSetBundleId(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.bundle_id = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookSetCodesignIdentity(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.codesign_identity = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookSetProvisioningProfile(
    interp: *const Interpreter,
    args: []const Value,
    bc: *BuildConfig,
    alloc: Allocator,
) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.provisioning_profile = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookGetBundlePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.bundle_path orelse "" };
}

fn hookGetBundleId(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.bundle_id orelse "" };
}

fn hookGetCodesignIdentity(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.codesign_identity orelse "" };
}

fn hookGetProvisioningProfile(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.provisioning_profile orelse "" };
}

// ── Target accessors ──────────────────────────────────────────────────
// These look at the target_triple that main.zig populates and answer
// the same questions TargetConfig's helpers do for Zig callers.

fn tripleContains(triple: ?[]const u8, needle: []const u8) bool {
    const t = triple orelse return false;
    return std.mem.indexOf(u8, t, needle) != null;
}

fn isIOSTriple(triple: ?[]const u8) bool {
    return tripleContains(triple, "apple-ios");
}

fn hookGetTargetTriple(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.target_triple orelse "" };
}

fn hookIsMacOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (isIOSTriple(bc.target_triple)) return Value{ .boolean = false };
    const t = bc.target_triple orelse "";
    const is_mac = std.mem.indexOf(u8, t, "apple-macosx") != null or
        std.mem.indexOf(u8, t, "apple-macos") != null or
        std.mem.indexOf(u8, t, "apple-darwin") != null;
    return Value{ .boolean = is_mac };
}

fn hookIsIOS(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .boolean = isIOSTriple(bc.target_triple) };
}

fn hookIsIOSDevice(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    const ios = isIOSTriple(bc.target_triple);
    const sim = tripleContains(bc.target_triple, "simulator");
    return Value{ .boolean = ios and !sim };
}

fn hookIsIOSSimulator(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    const ios = isIOSTriple(bc.target_triple);
    const sim = tripleContains(bc.target_triple, "simulator");
    return Value{ .boolean = ios and sim };
}

fn hookIsAndroid(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .boolean = tripleContains(bc.target_triple, "android") };
}

// ── Android-specific bundling setters + accessors ─────────────────────

fn hookSetManifestPath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.manifest_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookGetManifestPath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.manifest_path orelse "" };
}

fn hookSetKeystorePath(interp: *const Interpreter, args: []const Value, bc: *BuildConfig, alloc: Allocator) HookError!Value {
    if (args.len < 2) return .void_val;
    if (args[1].asString(interp)) |s| {
        bc.keystore_path = alloc.dupe(u8, s) catch return error.CannotEvalComptime;
    }
    return .void_val;
}

fn hookGetKeystorePath(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .string = bc.keystore_path orelse "" };
}

// ── #jni_main emission accessors ──────────────────────────────────────
// The Android bundler walks these as `0..jni_main_count()` and reads
// each entry's `(foreign_path, java_source)` pair so it can write a
// `.java` file per decl, compile via javac, and produce classes.dex
// via d8 before zipping into the APK.

fn hookJniMainCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return Value{ .int = @intCast(bc.jni_main_foreign_paths.len) };
}

fn hookJniMainForeignPathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx = args[1].asInt() orelse return error.TypeError;
    if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_foreign_paths.len) return Value{ .string = "" };
    return Value{ .string = bc.jni_main_foreign_paths[@intCast(idx)] };
}

fn hookJniMainJavaSourceAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx = args[1].asInt() orelse return error.TypeError;
    if (idx < 0 or @as(usize, @intCast(idx)) >= bc.jni_main_java_sources.len) return Value{ .string = "" };
    return Value{ .string = bc.jni_main_java_sources[@intCast(idx)] };
}

// ── Framework list accessors ──────────────────────────────────────────
// The Apple .app bundler in `library/modules/platform/bundle.sx` walks
// the framework list to recursively copy each `<Name>.framework`
// directory from the user's -F search paths into `<bundle>/Frameworks/`.
// Slice-of-string returns aren't natively expressible as a Value, so we
// expose count + indexed lookups instead.

fn intValue(n: i64) Value {
    return Value{ .int = n };
}

fn hookFrameworkCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return intValue(@intCast(bc.target_frameworks.len));
}

fn hookFrameworkAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx_i64 = args[1].asInt() orelse return error.TypeError;
    if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_frameworks.len) {
        return Value{ .string = "" };
    }
    return Value{ .string = bc.target_frameworks[@intCast(idx_i64)] };
}

fn hookFrameworkPathCount(_: *const Interpreter, _: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    return intValue(@intCast(bc.target_framework_paths.len));
}

fn hookFrameworkPathAt(_: *const Interpreter, args: []const Value, bc: *BuildConfig, _: Allocator) HookError!Value {
    if (args.len < 2) return Value{ .string = "" };
    const idx_i64 = args[1].asInt() orelse return error.TypeError;
    if (idx_i64 < 0 or @as(usize, @intCast(idx_i64)) >= bc.target_framework_paths.len) {
        return Value{ .string = "" };
    }
    return Value{ .string = bc.target_framework_paths[@intCast(idx_i64)] };
}
