const std = @import("std");
const uefi = std.os.uefi;
const Event = uefi.Event;
const EventRegistration = uefi.EventRegistration;
const Guid = uefi.Guid;
const Handle = uefi.Handle;
const Page = uefi.Page;
const Pages = uefi.Pages;
const Status = uefi.Status;
const TableHeader = uefi.tables.TableHeader;
const DevicePathProtocol = uefi.protocol.DevicePath;
const AllocateLocation = uefi.tables.AllocateLocation;
const AllocateType = uefi.tables.AllocateType;
const MemoryType = uefi.tables.MemoryType;
const MemoryDescriptor = uefi.tables.MemoryDescriptor;
const MemoryMapKey = uefi.tables.MemoryMapKey;
const MemoryMapInfo = uefi.tables.MemoryMapInfo;
const MemoryMapSlice = uefi.tables.MemoryMapSlice;
const TimerDelay = uefi.tables.TimerDelay;
const InterfaceType = uefi.tables.InterfaceType;
const LocateSearch = uefi.tables.LocateSearch;
const LocateSearchType = uefi.tables.LocateSearchType;
const OpenProtocolArgs = uefi.tables.OpenProtocolArgs;
const OpenProtocolAttributes = uefi.tables.OpenProtocolAttributes;
const ProtocolInformationEntry = uefi.tables.ProtocolInformationEntry;
const EventNotify = uefi.tables.EventNotify;
const cc = uefi.cc;
const Error = Status.Error;

/// Boot services are services provided by the system's firmware until the operating system takes
/// over control over the hardware by calling exitBootServices.
///
/// Boot Services must not be used after exitBootServices has been called. The only exception is
/// getMemoryMap, which may be used after the first unsuccessful call to exitBootServices.
/// After successfully calling exitBootServices, system_table.console_in_handle, system_table.con_in,
/// system_table.console_out_handle, system_table.con_out, system_table.standard_error_handle,
/// system_table.std_err, and system_table.boot_services should be set to null. After setting these
/// attributes to null, system_table.hdr.crc32 must be recomputed.
///
/// As the boot_services table may grow with new UEFI versions, it is important to check hdr.header_size.
pub const BootServices = extern struct {
    hdr: TableHeader,

    /// Raises a task's priority level and returns its previous level.
    raiseTpl: *const fn (new_tpl: TaskPriorityLevel) callconv(cc) TaskPriorityLevel,

    /// Restores a task's priority level to its previous value.
    restoreTpl: *const fn (old_tpl: TaskPriorityLevel) callconv(cc) void,

    /// Allocates memory pages from the system.
    _allocatePages: *const fn (alloc_type: AllocateType, mem_type: MemoryType, pages: usize, memory: *[*]align(4096) Page) callconv(cc) Status,

    /// Frees memory pages.
    _freePages: *const fn (memory: [*]align(4096) Page, pages: usize) callconv(cc) Status,

    /// Returns the current memory map.
    _getMemoryMap: *const fn (mmap_size: *usize, mmap: ?[*]align(@alignOf(MemoryDescriptor)) u8, map_key: *MemoryMapKey, descriptor_size: *usize, descriptor_version: *u32) callconv(cc) Status,

    /// Allocates pool memory.
    _allocatePool: *const fn (pool_type: MemoryType, size: usize, buffer: *[*]align(8) u8) callconv(cc) Status,

    /// Returns pool memory to the system.
    _freePool: *const fn (buffer: [*]align(8) u8) callconv(cc) Status,

    /// Creates an event.
    _createEvent: *const fn (type: u32, notify_tpl: TaskPriorityLevel, notify_func: ?*const fn (Event, ?*anyopaque) callconv(cc) void, notify_ctx: ?*anyopaque, event: *Event) callconv(cc) Status,

    /// Sets the type of timer and the trigger time for a timer event.
    _setTimer: *const fn (event: Event, type: TimerDelay, trigger_time: u64) callconv(cc) Status,

    /// Stops execution until an event is signaled.
    _waitForEvent: *const fn (event_len: usize, events: [*]const Event, index: *usize) callconv(cc) Status,

    /// Signals an event.
    _signalEvent: *const fn (event: Event) callconv(cc) Status,

    /// Closes an event.
    _closeEvent: *const fn (event: Event) callconv(cc) Status,

    /// Checks whether an event is in the signaled state.
    _checkEvent: *const fn (event: Event) callconv(cc) Status,

    /// Installs a protocol interface on a device handle. If the handle does not exist, it is created
    /// and added to the list of handles in the system. installMultipleProtocolInterfaces()
    /// performs more error checking than installProtocolInterface(), so its use is recommended over this.
    _installProtocolInterface: *const fn (handle: Handle, protocol: *const Guid, interface_type: InterfaceType, interface: *anyopaque) callconv(cc) Status,

    /// Reinstalls a protocol interface on a device handle
    _reinstallProtocolInterface: *const fn (handle: Handle, protocol: *const Guid, old_interface: *anyopaque, new_interface: *anyopaque) callconv(cc) Status,

    /// Removes a protocol interface from a device handle. Usage of
    /// uninstallMultipleProtocolInterfaces is recommended over this.
    _uninstallProtocolInterface: *const fn (handle: Handle, protocol: *const Guid, interface: *anyopaque) callconv(cc) Status,

    /// Queries a handle to determine if it supports a specified protocol.
    _handleProtocol: *const fn (handle: Handle, protocol: *const Guid, interface: *?*anyopaque) callconv(cc) Status,

    _reserved: *anyopaque,

    /// Creates an event that is to be signaled whenever an interface is installed for a specified protocol.
    _registerProtocolNotify: *const fn (protocol: *const Guid, event: Event, registration: *EventRegistration) callconv(cc) Status,

    /// Returns an array of handles that support a specified protocol.
    _locateHandle: *const fn (search_type: LocateSearchType, protocol: ?*const Guid, search_key: ?*const anyopaque, buffer_size: *usize, buffer: ?[*]Handle) callconv(cc) Status,

    /// Locates the handle to a device on the device path that supports the specified protocol
    _locateDevicePath: *const fn (protocols: *const Guid, device_path: **const DevicePathProtocol, device: *?Handle) callconv(cc) Status,

    /// Adds, updates, or removes a configuration table entry from the EFI System Table.
    _installConfigurationTable: *const fn (guid: *const Guid, table: ?*anyopaque) callconv(cc) Status,

    /// Loads an EFI image into memory.
    _loadImage: *const fn (boot_policy: bool, parent_image_handle: Handle, device_path: ?*const DevicePathProtocol, source_buffer: ?[*]const u8, source_size: usize, image_handle: *Handle) callconv(cc) Status,

    /// Transfers control to a loaded image's entry point.
    _startImage: *const fn (image_handle: Handle, exit_data_size: ?*usize, exit_data: ?*[*]u16) callconv(cc) Status,

    /// Terminates a loaded EFI image and returns control to boot services.
    _exit: *const fn (image_handle: Handle, exit_status: Status, exit_data_size: usize, exit_data: ?[*]align(2) const u8) callconv(cc) Status,

    /// Unloads an image.
    _unloadImage: *const fn (image_handle: Handle) callconv(cc) Status,

    /// Terminates all boot services.
    _exitBootServices: *const fn (image_handle: Handle, map_key: MemoryMapKey) callconv(cc) Status,

    /// Returns a monotonically increasing count for the platform.
    _getNextMonotonicCount: *const fn (count: *u64) callconv(cc) Status,

    /// Induces a fine-grained stall.
    _stall: *const fn (microseconds: usize) callconv(cc) Status,

    /// Sets the system's watchdog timer.
    _setWatchdogTimer: *const fn (timeout: usize, watchdog_code: u64, data_size: usize, watchdog_data: ?[*]const u16) callconv(cc) Status,

    /// Connects one or more drives to a controller.
    _connectController: *const fn (controller_handle: Handle, driver_image_handle: ?[*:null]?Handle, remaining_device_path: ?*const DevicePathProtocol, recursive: bool) callconv(cc) Status,

    // Disconnects one or more drivers from a controller
    _disconnectController: *const fn (controller_handle: Handle, driver_image_handle: ?Handle, child_handle: ?Handle) callconv(cc) Status,

    /// Queries a handle to determine if it supports a specified protocol.
    _openProtocol: *const fn (handle: Handle, protocol: *const Guid, interface: ?*?*anyopaque, agent_handle: ?Handle, controller_handle: ?Handle, attributes: OpenProtocolAttributes) callconv(cc) Status,

    /// Closes a protocol on a handle that was opened using openProtocol().
    _closeProtocol: *const fn (handle: Handle, protocol: *const Guid, agent_handle: Handle, controller_handle: ?Handle) callconv(cc) Status,

    /// Retrieves the list of agents that currently have a protocol interface opened.
    _openProtocolInformation: *const fn (handle: Handle, protocol: *const Guid, entry_buffer: *[*]ProtocolInformationEntry, entry_count: *usize) callconv(cc) Status,

    /// Retrieves the list of protocol interface GUIDs that are installed on a handle in a buffer allocated from pool.
    _protocolsPerHandle: *const fn (handle: Handle, protocol_buffer: *[*]*const Guid, protocol_buffer_count: *usize) callconv(cc) Status,

    /// Returns an array of handles that support the requested protocol in a buffer allocated from pool.
    _locateHandleBuffer: *const fn (search_type: LocateSearchType, protocol: ?*const Guid, search_key: ?*const anyopaque, num_handles: *usize, buffer: *[*]Handle) callconv(cc) Status,

    /// Returns the first protocol instance that matches the given protocol.
    _locateProtocol: *const fn (protocol: *const Guid, registration: ?EventRegistration, interface: *?*const anyopaque) callconv(cc) Status,

    /// Installs one or more protocol interfaces into the boot services environment
    // TODO: use callconv(cc) instead once that works
    _installMultipleProtocolInterfaces: *const fn (handle: *Handle, ...) callconv(.c) Status,

    /// Removes one or more protocol interfaces into the boot services environment
    // TODO: use callconv(cc) instead once that works
    _uninstallMultipleProtocolInterfaces: *const fn (handle: *Handle, ...) callconv(.c) Status,

    /// Computes and returns a 32-bit CRC for a data buffer.
    _calculateCrc32: *const fn (data: [*]const u8, data_size: usize, *u32) callconv(cc) Status,

    /// Copies the contents of one buffer to another buffer
    _copyMem: *const fn (dest: [*]u8, src: [*]const u8, len: usize) callconv(cc) void,

    /// Fills a buffer with a specified value
    _setMem: *const fn (buffer: [*]u8, size: usize, value: u8) callconv(cc) void,

    /// Creates an event in a group.
    _createEventEx: *const fn (type: u32, notify_tpl: usize, notify_func: EventNotify, notify_ctx: *const anyopaque, event_group: *const Guid, event: *Event) callconv(cc) Status,

    pub const AllocatePagesError = uefi.UnexpectedError || error{
        OutOfResources,
        InvalidParameter,
        NotFound,
    };

    pub const FreePagesError = uefi.UnexpectedError || error{
        NotFound,
        InvalidParameter,
    };

    pub const GetMemoryMapError = uefi.UnexpectedError || error{
        InvalidParameter,
        BufferTooSmall,
    };

    pub const AllocatePoolError = uefi.UnexpectedError || error{
        OutOfResources,
        InvalidParameter,
    };

    pub const FreePoolError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const CreateEventError = uefi.UnexpectedError || error{
        InvalidParameter,
        OutOfResources,
    };

    pub const SetTimerError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const WaitForEventError = uefi.UnexpectedError || error{
        InvalidParameter,
        Unsupported,
    };

    pub const CheckEventError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const ReinstallProtocolInterfaceError = uefi.UnexpectedError || error{
        NotFound,
        AccessDenied,
        InvalidParameter,
    };

    pub const HandleProtocolError = uefi.UnexpectedError || error{
        Unsupported,
    };

    pub const RegisterProtocolNotifyError = uefi.UnexpectedError || error{
        OutOfResources,
        InvalidParameter,
    };

    pub const NumHandlesError = uefi.UnexpectedError;

    pub const LocateHandleError = uefi.UnexpectedError || error{
        BufferTooSmall,
        InvalidParameter,
    };

    pub const LocateDevicePathError = uefi.UnexpectedError || error{
        NotFound,
        InvalidParameter,
    };

    pub const InstallConfigurationTableError = uefi.UnexpectedError || error{
        InvalidParameter,
        OutOfResources,
    };

    pub const UninstallConfigurationTableError = InstallConfigurationTableError || error{
        NotFound,
    };

    pub const LoadImageError = uefi.UnexpectedError || error{
        NotFound,
        InvalidParameter,
        Unsupported,
        OutOfResources,
        LoadError,
        DeviceError,
        AccessDenied,
        SecurityViolation,
    };

    pub const StartImageError = uefi.UnexpectedError || error{
        InvalidParameter,
        SecurityViolation,
    };

    pub const ExitError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const ExitBootServicesError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const GetNextMonotonicCountError = uefi.UnexpectedError || error{
        DeviceError,
        InvalidParameter,
    };

    pub const SetWatchdogTimerError = uefi.UnexpectedError || error{
        InvalidParameter,
        Unsupported,
        DeviceError,
    };

    pub const ConnectControllerError = uefi.UnexpectedError || error{
        InvalidParameter,
        NotFound,
        SecurityViolation,
    };

    pub const DisconnectControllerError = uefi.UnexpectedError || error{
        InvalidParameter,
        OutOfResources,
        DeviceError,
    };

    pub const OpenProtocolError = uefi.UnexpectedError || error{
        InvalidParameter,
        Unsupported,
        AccessDenied,
        AlreadyStarted,
    };

    pub const CloseProtocolError = uefi.UnexpectedError || error{
        InvalidParameter,
        NotFound,
    };

    pub const OpenProtocolInformationError = uefi.UnexpectedError || error{
        OutOfResources,
    };

    pub const ProtocolsPerHandleError = uefi.UnexpectedError || error{
        InvalidParameter,
        OutOfResources,
    };

    pub const LocateHandleBufferError = uefi.UnexpectedError || error{
        InvalidParameter,
        OutOfResources,
    };

    pub const LocateProtocolError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const InstallProtocolInterfacesError = uefi.UnexpectedError || error{
        AlreadyStarted,
        OutOfResources,
        InvalidParameter,
    };

    pub const UninstallProtocolInterfacesError = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    pub const CalculateCrc32Error = uefi.UnexpectedError || error{
        InvalidParameter,
    };

    /// Allocates pages of memory.
    ///
    /// This function scans the memory map to locate free pages. When it finds a
    /// physically contiguous block of pages that is large enough and also satisfies
    /// the allocation requirements of `alloc_type`, it changes the memory map to
    /// indicate that the pages are now of type `mem_type`.
    ///
    /// In general, UEFI OS loaders and UEFI applications should allocate memory
    /// (and pool) of type `.loader_data`. UEFI boot service drivers must allocate
    /// memory (and pool) of type `.boot_services_data`. UREFI runtime drivers
    /// should allocate memory (and pool) of type `.runtime_services_data`
    /// (although such allocation can only be made during boot services time).
    ///
    /// Allocation requests of `.allocate_any_pages` allocate any available range
    /// of pages that satisfies the request.
    ///
    /// Allocation requests of `.allocate_max_address` allocate any available range
    /// of pages whose uppermost address is less than or equal to the address
    /// pointed to by the input.
    ///
    /// Allocation requests of `.allocate_address` allocate pages at the address
    /// pointed to by the input.
    pub fn allocatePages(
        self: *BootServices,
        location: AllocateLocation,
        mem_type: MemoryType,
        pages: usize,
    ) AllocatePagesError![]align(4096) Page {
        var ptr: [*]align(4096) Page = switch (location) {
            .any => undefined,
            .address, .max_address => |ptr| ptr,
        };

        switch (self._allocatePages(
            std.meta.activeTag(location),
            mem_type,
            pages,
            &ptr,
        )) {
            .success => return ptr[0..pages],
            .out_of_resources => return error.OutOfResources,
            .invalid_parameter => return error.InvalidParameter,
            .not_found => return error.NotFound,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn freePages(self: *BootServices, pages: []align(4096) Page) FreePagesError!void {
        switch (self._freePages(pages.ptr, pages.len)) {
            .success => {},
            .not_found => return error.NotFound,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn getMemoryMapInfo(self: *const BootServices) uefi.UnexpectedError!MemoryMapInfo {
        var info: MemoryMapInfo = undefined;
        info.len = 0;

        switch (self._getMemoryMap(
            &info.len,
            null,
            &info.key,
            &info.descriptor_size,
            &info.descriptor_version,
        )) {
            .success, .buffer_too_small => {
                info.len = @divExact(info.len, info.descriptor_size);
                return info;
            },
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn getMemoryMap(
        self: *const BootServices,
        buffer: []align(@alignOf(MemoryDescriptor)) u8,
    ) GetMemoryMapError!MemoryMapSlice {
        var info: MemoryMapInfo = undefined;
        info.len = buffer.len;

        switch (self._getMemoryMap(
            &info.len,
            buffer.ptr,
            &info.key,
            &info.descriptor_size,
            &info.descriptor_version,
        )) {
            .success => {
                info.len = @divExact(info.len, info.descriptor_size);
                return .{ .info = info, .ptr = buffer.ptr };
            },
            .buffer_too_small => return error.BufferTooSmall,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Allocates a memory region of `size` bytes from memory of type `pool_type`
    /// and returns the allocated memory. Allocates pages from `.conventional_memory`
    /// as needed to grow the requested pool type.
    pub fn allocatePool(
        self: *BootServices,
        pool_type: MemoryType,
        size: usize,
    ) AllocatePoolError![]align(8) u8 {
        var ptr: [*]align(8) u8 = undefined;

        switch (self._allocatePool(pool_type, size, &ptr)) {
            .success => return ptr[0..size],
            .out_of_resources => return error.OutOfResources,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn freePool(self: *BootServices, ptr: [*]align(8) u8) FreePoolError!void {
        switch (self._freePool(ptr)) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn createEvent(
        self: *BootServices,
        event_type: uefi.EventType,
        notify_opts: NotifyOpts,
    ) CreateEventError!Event {
        var evt: Event = undefined;

        switch (self._createEvent(
            @bitCast(event_type),
            notify_opts.tpl,
            notify_opts.function,
            notify_opts.context,
            &evt,
        )) {
            .success => return evt,
            .invalid_parameter => return error.InvalidParameter,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Cancels any previous time trigger setting for the event, and sets a new
    /// trigger timer for the event.
    ///
    /// Returns `error.InvalidParameter` if the event is not a timer event.
    pub fn setTimer(
        self: *BootServices,
        event: Event,
        @"type": TimerDelay,
        trigger_time: u64,
    ) SetTimerError!void {
        switch (self._setTimer(event, @"type", trigger_time)) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Returns the event that was signaled, along with its index in the slice.
    pub fn waitForEvent(
        self: *BootServices,
        events: []const Event,
    ) WaitForEventError!struct { *const Event, usize } {
        var idx: usize = undefined;
        switch (self._waitForEvent(events.len, events.ptr, &idx)) {
            .success => return .{ &events[idx], idx },
            .invalid_parameter => return error.InvalidParameter,
            .unsupported => return error.Unsupported,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// If `event` is `EventType.signal`, then the event’s notification function
    /// is scheduled to be invoked at the event’s notification task priority level.
    /// This function may be invoked from any task priority level.
    ///
    /// If the supplied Event is a part of an event group, then all of the events
    /// in the event group are also signaled and their notification functions are
    /// scheduled.
    ///
    /// When signaling an event group, it is possible to create an event in the
    /// group, signal it and then close the event to remove it from the group.
    pub fn signalEvent(self: *BootServices, event: Event) uefi.UnexpectedError!void {
        switch (self._signalEvent(event)) {
            .success => {},
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn closeEvent(self: *BootServices, event: Event) uefi.UnexpectedError!void {
        switch (self._closeEvent(event)) {
            .success => {},
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Checks to see whether an event is signaled.
    ///
    /// The underlying function is equivalent to this pseudo-code:
    /// ```
    /// if (event.type.signal)
    ///     return error.InvalidParameter;
    ///
    /// if (event.signaled) {
    ///     event.signaled = false;
    ///     return true;
    /// }
    ///
    /// const notify = event.notification_function orelse return false;
    /// notify();
    ///
    /// if (event.signaled) {
    ///     event.signaled = false;
    ///     return true;
    /// }
    ///
    /// return false;
    /// ```
    pub fn checkEvent(self: *BootServices, event: Event) CheckEventError!bool {
        switch (self._checkEvent(event)) {
            .success => return true,
            .not_ready => return false,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// See `installProtocolInterfaces`.
    ///
    /// Does not call `self._installProtocolInterface`, because
    /// `self._installMultipleProtocolInterfaces` performs more error checks.
    pub fn installProtocolInterface(
        self: *BootServices,
        handle: ?Handle,
        interface: anytype,
    ) InstallProtocolInterfacesError!Handle {
        return self.installProtocolInterfaces(handle, .{
            interface,
        });
    }

    /// Reinstalls a protocol interface on a device handle.
    ///
    /// `new` may be the same as `old`. If it is, the registered protocol notifications
    /// occur for the handle without replacing the interface on the handle.
    ///
    /// Any process that has registered to wait for the installation of the interface
    /// is notified.
    ///
    /// The caller is responsible for ensuring that there are no references to `old`
    /// if it is being removed.
    pub fn reinstallProtocolInterface(
        self: *BootServices,
        handle: Handle,
        Protocol: type,
        old: ?*const Protocol,
        new: ?*const Protocol,
    ) ReinstallProtocolInterfaceError!void {
        if (!@hasDecl(Protocol, "guid"))
            @compileError("protocol is missing guid");

        switch (self._reinstallProtocolInterface(
            handle,
            &Protocol.guid,
            old,
            new,
        )) {
            .success => {},
            .not_found => return error.NotFound,
            .access_denied => return error.AccessDenied,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// See `uninstallProtocolInterfaces`.
    ///
    /// Does not call `self._uninstallProtocolInterface`, because
    /// `self._uninstallMultipleProtocolInterfaces` performs more error checks.
    pub fn uninstallProtocolInterface(
        self: *BootServices,
        handle: Handle,
        interface: anytype,
    ) UninstallProtocolInterfacesError!void {
        return self.uninstallProtocolInterfaces(handle, .{
            interface,
        });
    }

    /// Returns a pointer to the `Protocol` interface if it's supported by the
    /// handle.
    ///
    /// Note that UEFI implementations are no longer required to implement this
    /// function, so it's implemented using `openProtocol` instead.
    pub fn handleProtocol(
        self: *BootServices,
        Protocol: type,
        handle: Handle,
    ) HandleProtocolError!?*Protocol {
        // per https://uefi.org/specs/UEFI/2.10/07_Services_Boot_Services.html#efi-boot-services-handleprotocol
        // handleProtocol is basically `openProtocol` where:
        // 1. agent_handle is `uefi.handle` (aka handle passed to `EfiMain`)
        // 2. controller_handle is `null`
        // 3. attributes is `EFI_OPEN_PROTOCOL_BY_HANDLE_PROTOCOL`

        return self.openProtocol(
            Protocol,
            handle,
            .{ .by_handle_protocol = .{ .agent = uefi.handle } },
        ) catch |err| switch (err) {
            error.AlreadyStarted => return uefi.unexpectedStatus(.already_started),
            error.AccessDenied => return uefi.unexpectedStatus(.access_denied),
            error.InvalidParameter => return uefi.unexpectedStatus(.invalid_parameter),
            else => return @errorCast(err),
        };
    }

    pub fn registerProtocolNotify(
        self: *BootServices,
        Protocol: type,
        event: Event,
    ) RegisterProtocolNotifyError!EventRegistration {
        if (!@hasDecl(Protocol, "guid"))
            @compileError("Protocol is missing guid");

        var registration: EventRegistration = undefined;
        switch (self._registerProtocolNotify(
            &Protocol.guid,
            event,
            &registration,
        )) {
            .success => return registration,
            .out_of_resources => return error.OutOfResources,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Returns the number of handles that match the given search criteria.
    pub fn locateHandleLen(self: *const BootServices, search: LocateSearch) NumHandlesError!usize {
        var len: usize = 0;
        switch (self._locateHandle(
            std.meta.activeTag(search),
            if (search == .by_protocol) search.by_protocol else null,
            if (search == .by_register_notify) search.by_register_notify else null,
            &len,
            null,
        )) {
            // If len is zero, it should return not_found, otherwise buffer_too_small.
            // This is because it can/should only return success when a valid buffer is
            // passed with a non zero size, which is not the case.
            // Thus this status is considered unreachable and will return error.Unexpected
            // .success => unreachable,
            .buffer_too_small => return @divExact(len, @sizeOf(uefi.Handle)),
            .not_found => return 0,
            // This function accounts for all possible causes of this error code
            // as per the most recent UEFI spec 2.10A, therefore this branch is
            // considered unreachable and will return error.Unexpected instead
            // .invalid_parameter => unreachable
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// To determine the necessary size of `buffer`, call `locateHandleLen` first.
    pub fn locateHandle(
        self: *BootServices,
        search: LocateSearch,
        buffer: []Handle,
    ) LocateHandleError![]Handle {
        var len: usize = @sizeOf(Handle) * buffer.len;
        switch (self._locateHandle(
            std.meta.activeTag(search),
            if (search == .by_protocol) search.by_protocol else null,
            if (search == .by_register_notify) search.by_register_notify else null,
            &len,
            buffer.ptr,
        )) {
            .success => return buffer[0..@divExact(len, @sizeOf(Handle))],
            .not_found => return buffer[0..0],
            .buffer_too_small => return error.BufferTooSmall,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Locates all devices on `device_path` that support `Protocol`. Once the closest
    /// match to `device_path` is found, it returns the unmatched device path and handle.
    pub fn locateDevicePath(
        self: *const BootServices,
        device_path: *const DevicePathProtocol,
        Protocol: type,
    ) LocateHandleError!?struct { *const DevicePathProtocol, Handle } {
        if (!@hasDecl(Protocol, "guid"))
            @compileError("Protocol is missing guid");

        var dev_path = device_path;
        var device: ?Handle = undefined;
        switch (self._locateDevicePath(
            &Protocol.guid,
            &dev_path,
            &device,
        )) {
            .success => return .{ dev_path, device.? },
            .not_found => return null,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn installConfigurationTable(
        self: *BootServices,
        guid: *const Guid,
        table: *anyopaque,
    ) InstallConfigurationTableError!void {
        switch (self._installConfigurationTable(
            guid,
            table,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn uninstallConfigurationTable(
        self: *BootServices,
        guid: *const Guid,
    ) UninstallConfigurationTableError!void {
        switch (self._installConfigurationTable(
            guid,
            null,
        )) {
            .success => {},
            .not_found => return error.NotFound,
            .invalid_parameter => return error.InvalidParameter,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub const LoadImageSource = union(enum) {
        buffer: []const u8,
        device_path: *const DevicePathProtocol,
    };

    pub fn loadImage(
        self: *BootServices,
        boot_policy: bool,
        parent_image: Handle,
        source: LoadImageSource,
    ) LoadImageError!Handle {
        var handle: Handle = undefined;

        switch (self._loadImage(
            boot_policy,
            parent_image,
            if (source == .device_path) source.device_path else null,
            if (source == .buffer) source.buffer.ptr else null,
            if (source == .buffer) source.buffer.len else 0,
            &handle,
        )) {
            .success => return handle,
            .not_found => return error.NotFound,
            .invalid_parameter => return error.InvalidParameter,
            .unsupported => return error.Unsupported,
            .out_of_resources => return error.OutOfResources,
            .load_error => return error.LoadError,
            .device_error => return error.DeviceError,
            .access_denied => return error.AccessDenied,
            .security_violation => return error.SecurityViolation,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn startImage(self: *BootServices, image: Handle) StartImageError!ImageExitData {
        var exit_data_size: usize = undefined;
        var exit_data: [*]u16 = undefined;

        const exit_code = switch (self._startImage(
            image,
            &exit_data_size,
            &exit_data,
        )) {
            .invalid_parameter => return error.InvalidParameter,
            .security_violation => return error.SecurityViolation,
            else => |exit_code| exit_code,
        };

        if (exit_data_size == 0) return .{
            .code = exit_code,
            .description = null,
            .data = null,
        };

        const description_ptr: [*:0]const u16 = @ptrCast(exit_data);
        const description = std.mem.sliceTo(description_ptr, 0);

        return ImageExitData{
            .code = exit_code,
            .description = description,
            .data = exit_data[description.len + 1 .. exit_data_size],
        };
    }

    /// `message` must be allocated using `allocatePool`.
    pub fn exit(
        self: *BootServices,
        handle: Handle,
        status: Status,
        message: ?[:0]const u16,
    ) ExitError!void {
        switch (self._exit(
            handle,
            status,
            if (message) |msg| (2 * msg.len) + 1 else 0,
            if (message) |msg| @ptrCast(msg.ptr) else null,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |exit_status| return uefi.unexpectedStatus(exit_status),
        }
    }

    /// `message` should be a null-terminated u16 string followed by binary data
    /// allocated using `allocatePool`.
    pub fn exitWithData(
        self: *BootServices,
        handle: Handle,
        status: Status,
        data: []align(2) const u8,
    ) ExitError!void {
        switch (self._exit(handle, status, data.len, data.ptr)) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |exit_status| return uefi.unexpectedStatus(exit_status),
        }
    }

    /// The result is the exit code of the unload handler. Any error codes are
    /// `try/catch`-able, leaving only success and warning codes as the result.
    pub fn unloadImage(
        self: *BootServices,
        image: Handle,
    ) Status.Error!Status {
        const status = self._unloadImage(image);
        try status.err();
        return status;
    }

    pub fn exitBootServices(
        self: *BootServices,
        image: Handle,
        map_key: MemoryMapKey,
    ) ExitBootServicesError!void {
        switch (self._exitBootServices(image, map_key)) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn getNextMonotonicCount(
        self: *const BootServices,
        count: *u64,
    ) GetNextMonotonicCountError!void {
        switch (self._getNextMonotonicCount(count)) {
            .success => {},
            .device_error => return error.DeviceError,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn stall(self: *const BootServices, microseconds: usize) uefi.UnexpectedError!void {
        switch (self._stall(microseconds)) {
            .success => {},
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn setWatchdogTimer(
        self: *BootServices,
        timeout: usize,
        watchdog_code: u64,
        data: ?[]const u16,
    ) SetWatchdogTimerError!void {
        switch (self._setWatchdogTimer(
            timeout,
            watchdog_code,
            if (data) |d| d.len else 0,
            if (data) |d| d.ptr else null,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            .unsupported => return error.Unsupported,
            .device_error => return error.DeviceError,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// `driver_image` should be a null-terminated ordered list of handles.
    pub fn connectController(
        self: *BootServices,
        controller: Handle,
        driver_image: ?[*:null]?Handle,
        remaining_device_path: ?*const DevicePathProtocol,
        recursive: bool,
    ) ConnectControllerError!void {
        switch (self._connectController(
            controller,
            driver_image,
            remaining_device_path,
            recursive,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            .not_found => return error.NotFound,
            .security_violation => return error.SecurityViolation,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn disconnectController(
        self: *BootServices,
        controller: Handle,
        driver_image: ?Handle,
        child: ?Handle,
    ) DisconnectControllerError!void {
        switch (self._disconnectController(
            controller,
            driver_image,
            child,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            .out_of_resources => return error.OutOfResources,
            .device_error => return error.DeviceError,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Opens a protocol with a structure as the loaded image for a UEFI application
    ///
    /// If `flag` is `.test_protocol`, then the only valid return value is `null`,
    /// and `Status.unsupported` is returned. Otherwise, if `_openProtocol` returns
    /// `Status.unsupported`, then `null` is returned.
    pub fn openProtocol(
        self: *BootServices,
        Protocol: type,
        handle: Handle,
        attributes: OpenProtocolArgs,
    ) OpenProtocolError!?*Protocol {
        if (!@hasDecl(Protocol, "guid"))
            @compileError("Protocol is missing guid: " ++ @typeName(Protocol));

        const agent_handle: ?Handle, const controller_handle: ?Handle = switch (attributes) {
            inline else => |arg| .{ arg.agent, arg.controller },
        };

        var ptr: ?*Protocol = undefined;

        switch (self._openProtocol(
            handle,
            &Protocol.guid,
            @as(*?*anyopaque, @ptrCast(&ptr)),
            agent_handle,
            controller_handle,
            std.meta.activeTag(attributes),
        )) {
            .success => return if (attributes == .test_protocol) null else ptr,
            .unsupported => return if (attributes == .test_protocol) error.Unsupported else null,
            .access_denied => return error.AccessDenied,
            .already_started => return error.AlreadyStarted,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn closeProtocol(
        self: *BootServices,
        handle: Handle,
        Protocol: type,
        agent: Handle,
        controller: ?Handle,
    ) CloseProtocolError!void {
        if (!@hasDecl(Protocol, "guid"))
            @compileError("protocol is missing guid: " ++ @typeName(Protocol));

        switch (self._closeProtocol(
            handle,
            &Protocol.guid,
            agent,
            controller,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            .not_found => return error.NotFound,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn openProtocolInformation(
        self: *const BootServices,
        handle: Handle,
        Protocol: type,
    ) OpenProtocolInformationError!?[]ProtocolInformationEntry {
        var entries: [*]ProtocolInformationEntry = undefined;
        var len: usize = undefined;

        switch (self._openProtocolInformation(
            handle,
            &Protocol.guid,
            &entries,
            &len,
        )) {
            .success => return entries[0..len],
            .not_found => return null,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn protocolsPerHandle(
        self: *const BootServices,
        handle: Handle,
    ) ProtocolsPerHandleError![]*const Guid {
        var guids: [*]*const Guid = undefined;
        var len: usize = undefined;

        switch (self._protocolsPerHandle(
            handle,
            &guids,
            &len,
        )) {
            .success => return guids[0..len],
            .invalid_parameter => return error.InvalidParameter,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn locateHandleBuffer(
        self: *const BootServices,
        search: LocateSearch,
    ) LocateHandleBufferError!?[]Handle {
        var handles: [*]Handle = undefined;
        var len: usize = undefined;

        switch (self._locateHandleBuffer(
            std.meta.activeTag(search),
            if (search == .by_protocol) search.by_protocol else null,
            if (search == .by_register_notify) search.by_register_notify else null,
            &len,
            &handles,
        )) {
            .success => return handles[0..len],
            .invalid_parameter => return error.InvalidParameter,
            .not_found => return null,
            .out_of_resources => return error.OutOfResources,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn locateProtocol(
        self: *const BootServices,
        Protocol: type,
        registration: ?EventRegistration,
    ) LocateProtocolError!?*Protocol {
        var interface: *Protocol = undefined;

        switch (self._locateProtocol(
            &Protocol.guid,
            registration,
            @ptrCast(&interface),
        )) {
            .success => return interface,
            .not_found => return null,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    /// Installs a set of protocol interfaces into the boot services environment.
    ///
    /// This function's final argument should be a tuple of pointers to protocol
    /// interfaces. For example:
    ///
    /// ```
    /// const handle = try boot_services.installProtocolInterfaces(null, .{
    ///     &my_interface_1,
    ///     &my_interface_2,
    /// });
    /// ```
    ///
    /// The underlying function accepts a vararg list of pairs of Guid pointers
    /// and opaque pointers to the interface. To provide a guid, the interface
    /// types should declare a `guid` constant like so:
    ///
    /// ```
    /// pub const guid: uefi.Guid = .{ ... };
    /// ```
    ///
    /// See `std.os.uefi.protocol` for examples of protocol type definitions.
    pub fn installProtocolInterfaces(
        self: *BootServices,
        handle: ?Handle,
        interfaces: anytype,
    ) InstallProtocolInterfacesError!Handle {
        var hdl: ?Handle = handle;
        const args_tuple = protocolInterfaces(&hdl, interfaces);

        switch (@call(
            .auto,
            self._installMultipleProtocolInterfaces,
            args_tuple,
        )) {
            .success => return hdl.?,
            .already_started => return error.AlreadyStarted,
            .out_of_resources => return error.OutOfResources,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn uninstallProtocolInterfaces(
        self: *BootServices,
        handle: Handle,
        interfaces: anytype,
    ) UninstallProtocolInterfacesError!void {
        const args_tuple = protocolInterfaces(handle, interfaces);

        switch (@call(
            .auto,
            self._uninstallMultipleProtocolInterfaces,
            args_tuple,
        )) {
            .success => {},
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub fn calculateCrc32(
        self: *const BootServices,
        data: []const u8,
    ) CalculateCrc32Error!u32 {
        var value: u32 = undefined;
        switch (self._calculateCrc32(data.ptr, data.len, &value)) {
            .success => return value,
            .invalid_parameter => return error.InvalidParameter,
            else => |status| return uefi.unexpectedStatus(status),
        }
    }

    pub const signature: u64 = 0x56524553544f4f42;

    pub const NotifyOpts = struct {
        tpl: TaskPriorityLevel = .application,
        function: ?*const fn (Event, ?*anyopaque) callconv(cc) void = null,
        context: ?*anyopaque = null,
    };

    pub const TaskPriorityLevel = enum(usize) {
        application = 4,
        callback = 8,
        notify = 16,
        high_level = 31,
        _,
    };

    pub const ImageExitData = struct {
        code: Status,
        description: ?[:0]const u16,
        data: ?[]const u16,
    };
};

fn protocolInterfaces(
    handle_arg: anytype,
    interfaces: anytype,
) ProtocolInterfaces(@TypeOf(handle_arg), @TypeOf(interfaces)) {
    var result: ProtocolInterfaces(
        @TypeOf(handle_arg),
        @TypeOf(interfaces),
    ) = undefined;
    result[0] = handle_arg;

    var idx: usize = 1;
    inline for (interfaces) |interface| {
        const InterfacePtr = @TypeOf(interface);
        const Interface = switch (@typeInfo(InterfacePtr)) {
            .pointer => |pointer| pointer.child,
            else => @compileError("expected tuple of '*const Protocol', got " ++ @typeName(InterfacePtr)),
        };

        if (!@hasDecl(Interface, "guid"))
            @compileError("protocol interface '" ++ @typeName(Interface) ++
                "' does not declare a 'const guid: uefi.Guid'.");

        switch (@typeInfo(Interface)) {
            .@"struct" => |struct_info| if (struct_info.layout != .@"extern")
                @compileLog("protocol interface '" ++ @typeName(Interface) ++
                    "' is not extern - this is likely a mistake"),
            else => @compileError("protocol interface must be a struct, got " ++ @typeName(Interface)),
        }

        result[idx] = &Interface.guid;
        result[idx + 1] = @ptrCast(interface);
        idx += 2;
    }

    return result;
}

fn ProtocolInterfaces(HandleType: type, Interfaces: type) type {
    const interfaces_type_info = @typeInfo(Interfaces);
    if (interfaces_type_info != .@"struct" or !interfaces_type_info.@"struct".is_tuple)
        @compileError("expected tuple of protocol interfaces, got " ++ @typeName(Interfaces));
    const interfaces_info = interfaces_type_info.@"struct";

    var tuple_types: [interfaces_info.fields.len * 2 + 1]type = undefined;
    tuple_types[0] = HandleType;
    var idx = 1;
    while (idx < tuple_types.len) : (idx += 2) {
        tuple_types[idx] = *const Guid;
        tuple_types[idx + 1] = *const anyopaque;
    }

    return std.meta.Tuple(tuple_types[0..]);
}
