const std = @import("std");

///
/// Returns the return type of the function type `Fn`.
///
pub fn FnReturn(comptime Fn: type) type {
    const type_info = @typeInfo(Fn);

    if (type_info != .Fn) @compileError("`Fn` must be a function type");

    return type_info.Fn.return_type orelse void;
}

///
/// Returns a single-input single-output closure type where `In` represents the input type, `Out`
/// represents the output type, and `captures_size` represents the size of the closure context.
///
pub fn Function(comptime captures_size: usize, comptime In: type, comptime Out: type) type {
    return struct {
        applyErased: fn (*anyopaque, In) Out,
        context: [captures_size]u8,

        ///
        /// Function type.
        ///
        const Self = @This();

        ///
        /// Applies `input` to `self`, producing a result according to the current context data.
        ///
        pub fn apply(self: *Self, input: In) Out {
            return self.applyErased(&self.context, input);
        }

        ///
        /// Creates a new [Self] by capturing the `captures` value as the context and `call` as the
        /// as the behavior executed when [apply] or [applyErased] is called.
        ///
        /// The newly created [Self] is returned.
        ///
        pub fn capture(captures: anytype, comptime call: fn (@TypeOf(captures), In) Out) Self {
            const Captures = @TypeOf(captures);

            if (@sizeOf(Captures) > captures_size)
                @compileError("`captures` must be smaller than or equal to " ++
                    std.fmt.comptimePrint("{d}", .{captures_size}) ++ " bytes");

            const captures_align = @alignOf(Captures);

            var function = Self{
                .context = undefined,

                .applyErased = struct {
                    fn do(erased: *anyopaque, input: In) Out {
                        return call(@ptrCast(*Captures, @alignCast(
                            captures_align, erased)).*, input);
                    }
                }.do,
            };

            @ptrCast(*Captures, @alignCast(captures_align, &function.context)).* = captures;

            return function;
        }
    };
}