const std = @import("std");
const expectEqual = std.testing.expectEqual;
const E = std.os.wasi.errno_t;
const builtin = @import("builtin");

const c = @import("c");

const exporter = @import("../export.zig");
const Value = exporter.Value;
const js_fn = @import("../thunk/js-fn.zig");
const zig_fn = @import("../thunk/zig-fn.zig");
pub const AbortSignal = @import("../type/abort-signal.zig").AbortSignal;
pub const Generator = @import("../type/generator.zig").Generator;
pub const GeneratorOf = @import("../type/generator.zig").GeneratorOf;
pub const GeneratorArgOf = @import("../type/generator.zig").GeneratorArgOf;
pub const image = @import("../type/image.zig");
pub const Promise = @import("../type/promise.zig").Promise;
pub const PromiseOf = @import("../type/promise.zig").PromiseOf;
pub const PromiseArgOf = @import("../type/promise.zig").PromiseArgOf;
const util = @import("../type/util.zig");
const work_queue = @import("../type/work-queue.zig");
const fn_transform = @import("../zigft/fn-transform.zig");
const hooks = @import("native/hooks.zig");
const interface = @import("native/interface.zig");

const Module = interface.Module(Value);

pub fn WorkQueue(ns: type) type {
    return @import("../type/work-queue.zig").WorkQueue(ns, struct {
        pub fn onQueueInit() !void {
            try startMultithread();
        }

        pub fn onQueueDeinit() void {
            stopMultithread();
        }
    });
}

var gpa = std.heap.DebugAllocator(.{}).init;
const allocator = gpa.allocator();
var imports: Module.Imports = undefined;

threadlocal var instance: *Module.Host = undefined;
threadlocal var initialized: bool = false;

pub fn createBool(initializer: bool) !Value {
    var value: Value = undefined;
    if (imports.create_bool(instance, initializer, &value) != .SUCCESS) {
        return error.UnableToCreateBoolean;
    }
    return value;
}

pub fn createInteger(initializer: i32, is_unsigned: bool) !Value {
    var value: Value = undefined;
    if (imports.create_integer(instance, initializer, is_unsigned, &value) != .SUCCESS) {
        return error.UnableToCreateInteger;
    }
    return value;
}

pub fn createBigInteger(initializer: i64, is_unsigned: bool) !Value {
    var value: Value = undefined;
    if (imports.create_big_integer(instance, initializer, is_unsigned, &value) != .SUCCESS) {
        return error.UnableToCreateInteger;
    }
    return value;
}

pub fn createString(initializer: []const u8) !Value {
    var value: Value = undefined;
    if (imports.create_string(instance, initializer.ptr, initializer.len, &value) != .SUCCESS) {
        return error.UnableToCreateString;
    }
    return value;
}

pub fn createView(bytes: ?[*]const u8, len: usize, copying: bool, read_only: bool, export_handle: ?usize, byte_align: usize) !Value {
    var value: Value = undefined;
    if (imports.create_view(instance, bytes, len, copying, read_only, export_handle orelse 0, byte_align, &value) != .SUCCESS) {
        return error.UnableToCreateDataView;
    }
    return value;
}

pub fn createInstance(structure: Value, dv: Value, slots: ?Value) !Value {
    var value: Value = undefined;
    if (imports.create_instance(instance, structure, dv, slots, &value) != .SUCCESS) {
        return error.UnableToCreateStructureInstance;
    }
    return value;
}

pub fn createTemplate(dv: ?Value, slots: ?Value) !Value {
    var value: Value = undefined;
    if (imports.create_template(instance, dv, slots, &value) != .SUCCESS) {
        return error.UnableToCreateTemplate;
    }
    return value;
}

pub fn createList() !Value {
    var list: Value = undefined;
    if (imports.create_list(instance, &list) != .SUCCESS) {
        return error.UnableToCreateList;
    }
    return list;
}

pub fn createObject() !Value {
    var object: Value = undefined;
    if (imports.create_object(instance, &object) != .SUCCESS) {
        return error.UnableToCreateObject;
    }
    return object;
}

pub fn getProperty(object: Value, key: []const u8) !Value {
    var value: Value = undefined;
    if (imports.get_property(instance, object, key.ptr, key.len, &value) != .SUCCESS) {
        return error.UnableToGetProperty;
    }
    return value;
}

pub fn setProperty(object: Value, key: []const u8, value: Value) !void {
    if (imports.set_property(instance, object, key.ptr, key.len, value) != .SUCCESS) {
        return error.UnableToSetProperty;
    }
}

pub fn getSlotValue(object: Value, slot: usize) !Value {
    var value: Value = undefined;
    if (imports.get_slot_value(instance, object, slot, &value) != .SUCCESS) {
        return error.UnableToGetSlotValue;
    }
    return value;
}

pub fn setSlotValue(object: Value, slot: usize, value: ?Value) !void {
    if (imports.set_slot_value(instance, object, slot, value) != .SUCCESS) {
        return error.UnableToSetSlotValue;
    }
}

pub fn appendList(list: Value, value: Value) !void {
    if (imports.append_list(instance, list, value) != .SUCCESS) {
        return error.UnableToAppendList;
    }
}

pub fn getExportHandle(comptime ptr: anytype) usize {
    const ns = struct {
        fn getAddress() usize {
            return @intFromPtr(ptr);
        }
    };
    return @intFromPtr(&ns.getAddress);
}

pub fn getStructure(name: []const u8) !Value {
    var value: Value = undefined;
    if (imports.get_structure(instance, name.ptr, name.len, &value) != .SUCCESS) {
        return error.UnableToGetStructure;
    }
    return value;
}

pub fn setStructure(name: []const u8, value: ?Value) !void {
    if (imports.set_structure(instance, name.ptr, name.len, value) != .SUCCESS) {
        return error.UnableToSetStructure;
    }
}

pub fn beginStructure(structure: Value) !void {
    if (imports.begin_structure(instance, structure) != .SUCCESS) {
        return error.UnableToDefineStructure;
    }
}

pub fn finishStructure(structure: Value) !void {
    if (imports.finish_structure(instance, structure) != .SUCCESS) {
        return error.UnableToDefineStructure;
    }
}

pub fn enableCallback(structure: Value, template: Value, member_flags: Value) !void {
    if (imports.enable_callback(instance, structure, template, member_flags) != .SUCCESS) {
        return error.UnableToEnableCallback;
    }
}

pub fn handleJscall(fn_id: usize, arg_ptr: *anyopaque, arg_size: usize) E {
    if (!initialized) @panic("Uninitialized thread");
    var call: interface.Jscall = .{
        .fn_id = fn_id,
        .arg_address = @intFromPtr(arg_ptr),
        .arg_size = arg_size,
    };
    return imports.handle_jscall(instance, &call);
}

pub fn releaseFunction(fn_ptr: anytype) void {
    const thunk_address = @intFromPtr(fn_ptr);
    const control = js_fn.createTargetController(@This(), @TypeOf(fn_ptr));
    const fn_id = control(.identify, thunk_address) catch return;
    _ = imports.release_function(instance, fn_id);
}

pub fn startMultithread() !void {
    if (imports.enable_multithread(instance) != .SUCCESS) return error.UnableToUseThread;
}

pub fn stopMultithread() void {
    _ = imports.disable_multithread(instance);
}

pub fn redirectIO(fn_ptr: *const anyopaque) !void {
    if (imports.redirect_syscalls(instance, fn_ptr) != .SUCCESS) return error.UnableToRedirectIo;
}

pub fn getInstance() *anyopaque {
    return instance;
}

pub fn setHostInstance(ptr: *Module.Host) callconv(.c) E {
    if (builtin.link_libc and builtin.target.os.tag != .windows) {
        if (@hasDecl(c, "RTLD_DEEPBIND")) {
            // fix missing environ due to RTLD_DEEPBIND option given to dlopen()
            if (std.c.dlopen(null, .{ .LAZY = true, .NOLOAD = true })) |handle| {
                defer _ = std.c.dlclose(handle);
                if (std.c.dlsym(handle, "environ")) |symbol| {
                    const environ_ptr: @TypeOf(&std.c.environ) = @ptrCast(@alignCast(symbol));
                    std.c.environ = environ_ptr.*;
                }
            }
        }
    }
    instance = ptr;
    initialized = true;
    return E.SUCCESS;
}

pub fn initializeThread(ptr: *anyopaque) !void {
    if (imports.initialize_thread(@ptrCast(ptr)) != .SUCCESS) {
        return error.UnableToInitializeThread;
    }
}

pub fn deinitializeThread(ptr: *anyopaque) !void {
    if (imports.deinitialize_thread(@ptrCast(ptr)) != .SUCCESS) {
        return error.UnableToDeinitializeThread;
    }
}

pub fn getExportAddress(handle: usize, dest: *usize) callconv(.c) E {
    const f: *const fn () usize = @ptrFromInt(handle);
    dest.* = f();
    return .SUCCESS;
}

pub fn getLanguageName() []const u8 {
    return language_name;
}

const empty_ptr: *anyopaque = @ptrCast(@constCast(&.{}));

fn runThunk(
    thunk_address: usize,
    fn_address: usize,
    arg_address: usize,
) callconv(.c) E {
    const thunk: zig_fn.Thunk = @ptrFromInt(thunk_address);
    const fn_ptr: *anyopaque = @ptrFromInt(fn_address);
    const arg_ptr: *anyopaque = if (arg_address != 0) @ptrFromInt(arg_address) else empty_ptr;
    return if (thunk(fn_ptr, arg_ptr)) .SUCCESS else |_| .FAULT;
}

fn runVariadicThunk(
    thunk_address: usize,
    fn_address: usize,
    arg_address: usize,
    attr_address: usize,
    arg_count: usize,
) callconv(.c) E {
    const thunk: zig_fn.VariadicThunk = @ptrFromInt(thunk_address);
    const fn_ptr: *anyopaque = @ptrFromInt(fn_address);
    const arg_ptr: *anyopaque = if (arg_address != 0) @ptrFromInt(arg_address) else empty_ptr;
    const attr_ptr: *const anyopaque = @ptrFromInt(attr_address);
    return if (thunk(fn_ptr, arg_ptr, attr_ptr, arg_count)) .SUCCESS else |_| .FAULT;
}

fn createJsThunk(
    controller_address: usize,
    fn_id: usize,
    dest: *usize,
) callconv(.c) E {
    const controller: js_fn.ThunkController = @ptrFromInt(controller_address);
    const thunk_address = controller(.create, fn_id) catch return .FAULT;
    dest.* = thunk_address;
    return .SUCCESS;
}

fn destroyJsThunk(
    controller_address: usize,
    fn_address: usize,
    dest: *usize,
) callconv(.c) E {
    const controller: js_fn.ThunkController = @ptrFromInt(controller_address);
    const fn_id = controller(.destroy, fn_address) catch return .FAULT;
    dest.* = fn_id;
    return .SUCCESS;
}

var language_name: []const u8 = "JavaScript";

fn setLanguageName(name: [*:0]const u8) callconv(.c) E {
    language_name = std.mem.sliceTo(name, 0);
    return .SUCCESS;
}

const hook_table = hooks.getHookTable(@This(), exporter.options.use_redirection);

fn getSyscallHook(name: [*:0]const u8, dest: *hooks.Entry) callconv(.c) E {
    const os = switch (builtin.target.os.tag) {
        .linux => .linux,
        .driverkit, .ios, .macos, .tvos, .visionos, .watchos => .darwin,
        .windows => .windows,
        else => .unknown,
    };
    var name_s = name[0..std.mem.len(name)];
    const entry = hook_table.get(name_s) orelse alt: {
        switch (os) {
            .darwin => {
                if (std.mem.indexOf(u8, name_s, "$")) |suffic_index| {
                    name_s.len = suffic_index;
                    if (hook_table.get(name_s)) |alt_entry| break :alt alt_entry;
                }
            },
            .windows => {
                if (std.mem.startsWith(u8, name_s, "_")) {
                    name_s = name_s[1..];
                    if (hook_table.get(name_s)) |alt_entry| break :alt alt_entry;
                    if (std.mem.endsWith(u8, name_s, "32")) {
                        name_s.len -= 2;
                        if (hook_table.get(name_s)) |alt_entry| break :alt alt_entry;
                    }
                } else if (std.mem.endsWith(u8, name_s, "A")) {
                    name_s.len -= 1;
                    if (hook_table.get(name_s)) |alt_entry| break :alt alt_entry;
                }
            },
            else => {},
        }
        return .NOENT;
    };
    dest.* = entry;
    return .SUCCESS;
}

pub fn redirectSyscall(call: *hooks.Syscall) std.c.E {
    // this function is needed even when use_redirection is false because of getenv()
    const result = switch (initialized) {
        true => imports.handle_syscall(instance, call),
        false => .FAULT,
    };
    // translate from WASI enum to the current system's
    const info = @typeInfo(E).@"enum";
    return inline for (info.field_names) |field_name| {
        const wasi_enum = @field(E, field_name);
        if (wasi_enum == result) {
            break switch (@hasField(std.c.E, field_name)) {
                true => @field(std.c.E, field_name),
                false => .FAULT,
            };
        }
    } else .FAULT;
}

threadlocal var redirection_suppressed: bool = false;

pub fn isRedirectingStderr() bool {
    return !redirection_suppressed;
}

pub fn isRedirecting(comptime literal: @TypeOf(.enum_literal)) bool {
    if (!exporter.options.use_redirection) unreachable;
    if (redirection_suppressed) return false;
    var mask: hooks.Syscall.Mask = undefined;
    if (imports.get_syscall_mask(instance, &mask) != .SUCCESS) return false;
    if (literal == .any) {
        const info = @typeInfo(hooks.Syscall.Mask).@"struct";
        return inline for (info.field_names) |field_name| {
            if (@field(mask, field_name)) break true;
        } else false;
    } else {
        const name = @tagName(literal);
        return @field(mask, name);
    }
}

pub fn createModule(comptime module_ns: type) Module {
    const host = @This();
    const ns = struct {
        fn getFactoryThunk(dest: *usize) callconv(.c) E {
            dest.* = @intFromPtr(exporter.getFactoryThunk(host, module_ns));
            return .SUCCESS;
        }
    };
    return .{
        .attributes = .{
            .little_endian = builtin.target.cpu.arch.endian() == .little,
            .runtime_safety = switch (builtin.mode) {
                .debug, .safe => true,
                else => false,
            },
            .libc = builtin.link_libc,
            .io_redirection = exporter.options.use_redirection,
            .debug = builtin.mode == .debug,
            .persistent = exporter.options.persistent,
        },
        .module_path = switch (builtin.mode) {
            .debug => exporter.options.module_path.ptr,
            else => @ptrCast(std.fs.path.basename(exporter.options.module_path).ptr),
        },
        .imports = &imports,
        .exports = &.{
            .set_host_instance = setHostInstance,
            .get_export_address = getExportAddress,
            .get_factory_thunk = ns.getFactoryThunk,
            .run_thunk = runThunk,
            .run_variadic_thunk = runVariadicThunk,
            .create_js_thunk = createJsThunk,
            .destroy_js_thunk = destroyJsThunk,
            .get_syscall_hook = getSyscallHook,
            .set_language_name = setLanguageName,
        },
    };
}

test "createModule" {
    const Test = struct {
        pub const a: i32 = 1;
        const b: i32 = 2;
        pub var c: bool = true;
        pub const d: f64 = 3.14;
        pub const e: [4]i32 = .{ 3, 4, 5, 6 };
        pub const f = enum { dog, cat, chicken };
        pub const g = enum(c_int) { dog = -100, cat, chicken };
        pub fn h(arg1: i32, arg2: i32) bool {
            return arg1 < arg2;
        }
    };
    const m = createModule(Test);
    try expectEqual(Module.current_revision, m.revision);
    try expectEqual((builtin.target.cpu.arch.endian() == .little), m.attributes.little_endian);
}

pub fn panic(msg: []const u8, _: ?*std.builtin.StackTrace, ret_addr: ?usize) noreturn {
    redirection_suppressed = true;
    std.debug.defaultPanic(msg, ret_addr);
}
