const std = @import("std");

// Notes:
//
// "init" is an overloaded term. Try not to use it. Instead, have hooks:
// - startup / shutdown. executed once each per program lifecycle.
// - load / unload. executed once per module load cycle.
//
// So the flow is:
// - startup. prepares long-lived state.
// - load. deserializes and prepares short-lived state.
// - unload. serializes and releases short-lived state.
// - shutdown. releases long-lived state.
//
// So when the hotplug system reloads the module, it performs an unload/load cycle.
// Normal operation without any reloads includes all four stages.
//
// Some considerations:
// - a module should be able to indicate specific failure:
//   - startup (preparing state)
//   - load (deserialization or preparing state)
//   - unload (serialization)
// - a module should be able to indicate it cannot be reloaded.
// - serialization data might come from disk, or from a reload, or be missing.
//
// On reloading and rollback:
// - when loading a module, copy the dynlib to a versioned temporary file, and load that temporary file.
// - procedure for reloading a module:
//   - current old version unload (and serialize)
//   - attempt new version load (and deserialize)
//   - if success: release old version and delete lib
//   - if failure:
//      - release new version and delete lib.
//      - attempt old version load (and deserialize) (catch fatal error)
// - Configure signal handlers to always clean up the temporary files on errors like segfault?
//   - temp files needs to gracefully handle multiple instances of the app running at once with shared hotloading.
//
// Things should be done in a thread-safe manner. Signal to a module that it should unload gracefully, and the
// module signal when it can safely be unloaded.

pub fn Slot(Function: type) type {
    const Arguments = std.meta.ArgsTuple(Function);
    const Return = @typeInfo(Function).@"fn".return_type.?;

    return extern struct {
        ptr: *const fn (*const Arguments, *Return) callconv(.c) void,

        pub fn wrap(impl: Function) @This() {
            return .{
                .ptr = &struct {
                    pub fn cimpl(args: *const Arguments, ret: *Return) callconv(.c) void {
                        ret.* = @call(.auto, impl, args.*);
                    }
                }.cimpl,
            };
        }

        pub fn invoke(self: @This(), args: Arguments) Return {
            var ret: Return = undefined;
            self.ptr(&args, &ret);
            return ret;
        }
    };
}

pub fn Module(State: type) type {
    return extern struct {
        pub const Error = std.mem.Allocator.Error;

        pub const StartupError = Error;
        pub const UnloadError = Error;
        pub const ReloadError = Error;

        const Startup = Slot(fn (std.mem.Allocator) StartupError!*State);
        const Unload = Slot(fn (*State, std.mem.Allocator) UnloadError![]u8);
        const Reload = Slot(fn (*State, std.mem.Allocator, []u8) ReloadError!void);
        const Shutdown = Slot(fn (*State, std.mem.Allocator) void);

        startup: Startup,
        unload: Unload,
        reload: Reload,
        shutdown: Shutdown,
    };
}

pub fn module(State: type) Module(State) {
    return .{
        .startup = Module(State).Startup.wrap(State.startup),
        .unload = Module(State).Unload.wrap(State.unload),
        .reload = Module(State).Reload.wrap(State.reload),
        .shutdown = Module(State).Shutdown.wrap(State.shutdown),
    };
}

pub const AnyModule = Module(anyopaque);
