#ifndef LINK_WIRELESS_MULTIBOOT_H
#define LINK_WIRELESS_MULTIBOOT_H

// --------------------------------------------------------------------------
// A Wireless Multiboot tool to send small ROMs from a GBA to up to 4 slaves.
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
//       LinkWirelessMultiboot* linkWirelessMultiboot =
//         new LinkWirelessMultiboot();
// - 2) Send the ROM:
//       LinkWirelessMultiboot::Result result = linkWirelessMultiboot->sendRom(
//         romBytes, // for current ROM, use: ((const u8*)MEM_EWRAM)
//         romLength, // in bytes
//         "Multiboot", // game name
//         "Test", // user name
//         0xFFFF, // game ID
//         2, // number of players
//         [](LinkWirelessMultiboot::MultibootProgress progress) {
//           // check progress.[state,connectedClients,percentage]
//
//           u16 keys = ~REG_KEYS & KEY_ANY;
//           return keys & KEY_START;
//           // (when this returns true, the transfer will be canceled)
//         }
//       );
//       // `result` should be LinkWirelessMultiboot::Result::SUCCESS
// - 3) (Optional) Send ROMs asynchronously:
//       LinkWirelessMultiboot::Async* linkWirelessMultibootAsync =
//         new LinkWirelessMultiboot::Async("Multiboot", "Test");
//       interrupt_init();
//       interrupt_add(INTR_VBLANK, LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_VBLANK);
//       interrupt_add(INTR_SERIAL, LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_SERIAL);
//       interrupt_add(INTR_TIMER3, LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_TIMER);
//       bool success = linkWirelessMultibootAsync->sendRom(
//         romBytes, romLength
//       );
//       if (success) {
//         // (monitor `playerCount()` and `getPercentage()`)
//         if (!linkWirelessMultibootAsync->isSending()) {
//           auto result = linkWirelessMultibootAsync->getResult();
//           // `result` should be
//           // LinkWirelessMultiboot::Async::GeneralResult::SUCCESS
//         }
//       }
// --------------------------------------------------------------------------

#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif

#include "_link_common.hpp"

#include "LinkRawWireless.hpp"
#include "LinkWirelessOpenSDK.hpp"

#ifndef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
/**
 * @brief Enable logging.
 * \warning Set `linkWirelessMultiboot->logger` and uncomment to enable!
 * \warning This option #includes std::string!
 */
// #define LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
#endif

#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
/**
 * @brief Disable nested IRQs (uncomment to enable).
 * In the async version, SERIAL IRQs can be interrupted (once they clear their
 * time-critical needs) by default, which helps prevent issues with audio
 * engines. However, if something goes wrong, you can disable this behavior.
 */
// #define LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
#endif

LINK_VERSION_TAG LINK_WIRELESS_MULTIBOOT_VERSION =
    "vLinkWirelessMultiboot/v8.0.0";

#define LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE (0x100 + 0xC0)
#define LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE (256 * 1024)
#define LINK_WIRELESS_MULTIBOOT_MIN_PLAYERS 2
#define LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS 5
#define LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_INTERVAL 50
#define LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_TIMER_ID 3
#define LINK_WIRELESS_MULTIBOOT_TRY(CALL)       \
  LINK_BARRIER;                                 \
  if ((lastResult = CALL) != Result::SUCCESS) { \
    return finish(lastResult);                  \
  }
#define LINK_WIRELESS_MULTIBOOT_TRY_SUB(CALL)   \
  LINK_BARRIER;                                 \
  if ((lastResult = CALL) != Result::SUCCESS) { \
    return lastResult;                          \
  }

#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
#include <string>
#define _LWMLOG_(str) logger(str)
#else
#define _LWMLOG_(str)
#endif

/**
 * @brief A Multiboot tool to send small ROMs from a GBA to up to 4 slaves via
 * GBA Wireless Adapter.
 */
class LinkWirelessMultiboot {
 private:
  using u32 = Link::u32;
  using u16 = Link::u16;
  using u8 = Link::u8;

  using CommState = LinkWirelessOpenSDK::CommState;
  using Sequence = LinkWirelessOpenSDK::SequenceNumber;
  using ClientHeader = LinkWirelessOpenSDK::ClientSDKHeader;
  using ClientPacket = LinkWirelessOpenSDK::ClientPacket;
  using ChildrenData = LinkWirelessOpenSDK::ChildrenData;
  using SendBuffer =
      LinkWirelessOpenSDK::SendBuffer<LinkWirelessOpenSDK::ServerSDKHeader>;

  static constexpr int HEADER_SIZE = 0xC0;
  static constexpr int SETUP_TX = 1;
  static constexpr int GAME_ID_MULTIBOOT_FLAG = 1 << 15;
  static constexpr int FRAME_LINES = 228;
  static constexpr int MAX_INFLIGHT_PACKETS = 4;
  static constexpr int FINAL_CONFIRMS = 3;
  static constexpr u8 CMD_START[] = {0x00, 0x54, 0x00, 0x00, 0x00, 0x02, 0x00};
  static constexpr int CMD_START_SIZE = 7;
  static constexpr u8 BOOTLOADER_HANDSHAKE[][6] = {
      {0x00, 0x00, 0x52, 0x46, 0x55, 0x2D},
      {0x4D, 0x42, 0x2D, 0x44, 0x4C, 0x00}};
  static constexpr int BOOTLOADER_HANDSHAKE_SIZE = 6;
  static constexpr u8 ROM_HEADER_PATCH[] = {0x52, 0x46, 0x55, 0x2D, 0x4D, 0x42,
                                            0x4F, 0x4F, 0x54, 0x00, 0x00, 0x00};
  static constexpr int ROM_HEADER_PATCH_OFFSET = 4;
  static constexpr int ROM_HEADER_PATCH_SIZE = 12;

 public:
#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
  typedef void (*Logger)(std::string);
  Logger logger = [](std::string str) {};
#endif

  enum class State {
    STOPPED = 0,
    INITIALIZING = 1,
    LISTENING = 2,
    PREPARING = 3,
    SENDING = 4,
    CONFIRMING = 5
  };

  enum class Result {
    SUCCESS = 0,
    INVALID_SIZE = 1,
    INVALID_PLAYERS = 2,
    CANCELED = 3,
    ADAPTER_NOT_DETECTED = 4,
    BAD_HANDSHAKE = 5,
    CLIENT_DISCONNECTED = 6,
    FAILURE = 7
  };

  struct MultibootProgress {
    State state = State::STOPPED;
    u8 connectedClients = 0;
    u8 percentage = 0;
    volatile bool* ready = nullptr;
  };

  /**
   * @brief Sends the `rom`. Once completed, the return value should be
   * `LinkWirelessMultiboot::Result::SUCCESS`.
   * @param rom A pointer to ROM data.
   * @param romSize Size of the ROM in bytes. It must be a number between
   * `448` and `262144`. It's recommended to use a ROM size that is a multiple
   * of `16`, as this also ensures compatibility with Multiboot via Link Cable.
   * @param gameName Game name. Maximum `14` characters + null terminator.
   * @param userName User name. Maximum `8` characters + null terminator.
   * @param gameId `(0 ~ 0x7FFF)` Game ID.
   * @param players The number of consoles that will download the ROM.
   * Once this number of players is reached, the code will start transmitting
   * the ROM bytes.
   * @param listener A function that will be continuously invoked. If it returns
   * `true`, the transfer will be aborted. It receives a
   * `LinkWirelessMultiboot::MultibootProgress` object with details.
   * @param keepConnectionAlive If `true`, the adapter won't be reset after a
   * successful transfer, so users can continue the session using
   * `LinkWireless::restoreExistingConnection()`.
   * \warning You can start the transfer before the player count is reached by
   * running `*progress.ready = true;` in the `listener` callback.
   * \warning Blocks the system until completion or cancellation.
   */
  template <typename C>
  Result sendRom(const u8* rom,
                 u32 romSize,
                 const char* gameName,
                 const char* userName,
                 const u16 gameId,
                 u8 players,
                 C listener,
                 bool keepConnectionAlive = false) {
    LINK_READ_TAG(LINK_WIRELESS_MULTIBOOT_VERSION);

    if (romSize < LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE ||
        romSize > LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE)
      return Result::INVALID_SIZE;
    if (players < LINK_WIRELESS_MULTIBOOT_MIN_PLAYERS ||
        players > LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS)
      return Result::INVALID_PLAYERS;

    resetState();

    _LWMLOG_("starting...");
    LINK_WIRELESS_MULTIBOOT_TRY(activate())
    progress.state = State::INITIALIZING;
    LINK_WIRELESS_MULTIBOOT_TRY(initialize(gameName, userName, gameId, players))

    _LWMLOG_("waiting for connections...");
    progress.state = State::LISTENING;
    LINK_WIRELESS_MULTIBOOT_TRY(waitForClients(players, listener))

    _LWMLOG_("all players are connected");
    progress.state = State::PREPARING;

    _LWMLOG_("rom start command...");
    LINK_WIRELESS_MULTIBOOT_TRY(sendRomStartCommand(listener))

    _LWMLOG_("SENDING ROM!");
    progress.state = State::SENDING;
    LINK_WIRELESS_MULTIBOOT_TRY(sendRomBytes(rom, romSize, listener))

    progress.state = State::CONFIRMING;
    LINK_WIRELESS_MULTIBOOT_TRY(confirm(listener))

    _LWMLOG_("SUCCESS!");
    return finish(Result::SUCCESS, keepConnectionAlive);
  }

  /**
   * @brief Turns off the adapter and deactivates the library. It returns a
   * boolean indicating whether the transition to low consumption mode was
   * successful.
   */
  bool reset() {
    bool success = linkRawWireless.bye();
    linkRawWireless.deactivate();
    resetState();
    return success;
  }

#ifdef LINK_RAW_WIRELESS_ENABLE_LOGGING
  /**
   * @brief Sets a logger function.
   * \warning This is internal API!
   */
  void _setLogger(LinkRawWireless::Logger logger) {
    linkRawWireless.logger = logger;
  }
#endif

 private:
  LinkRawWireless linkRawWireless;
  LinkWirelessOpenSDK linkWirelessOpenSDK;
  MultibootProgress progress;
  volatile bool readyFlag = false;
  volatile Result lastResult;
  ClientHeader lastValidHeader;

  Result activate() {
    if (!linkRawWireless.activate()) {
      _LWMLOG_("! adapter not detected");
      return Result::ADAPTER_NOT_DETECTED;
    }
    _LWMLOG_("activated");

    return Result::SUCCESS;
  }

  Result initialize(const char* gameName,
                    const char* userName,
                    const u16 gameId,
                    u8 players) {
    if (!linkRawWireless.setup(players, SETUP_TX)) {
      _LWMLOG_("! setup failed");
      return Result::FAILURE;
    }
    _LWMLOG_("setup ok");

    if (!linkRawWireless.broadcast(gameName, userName,
                                   gameId | GAME_ID_MULTIBOOT_FLAG)) {
      _LWMLOG_("! broadcast failed");
      return Result::FAILURE;
    }
    _LWMLOG_("broadcast data set");

    if (!linkRawWireless.startHost()) {
      _LWMLOG_("! start host failed");
      return Result::FAILURE;
    }
    _LWMLOG_("host started");

    return Result::SUCCESS;
  }

  template <typename C>
  Result waitForClients(u8 players, C listener) {
    LinkRawWireless::PollConnectionsResponse pollResponse;

    u32 currentPlayers = 1;
    while ((linkRawWireless.playerCount() < players && !readyFlag) ||
           linkRawWireless.playerCount() <= 1) {
      if (listener(progress))
        return Result::CANCELED;

      if (!linkRawWireless.pollConnections(pollResponse))
        return Result::FAILURE;

      if (linkRawWireless.playerCount() > currentPlayers) {
        currentPlayers = linkRawWireless.playerCount();
        progress.connectedClients = currentPlayers - 1;

        u8 lastClientNumber =
            pollResponse.connectedClients[pollResponse.connectedClientsSize - 1]
                .clientNumber;
        LINK_WIRELESS_MULTIBOOT_TRY_SUB(
            handshakeClient(lastClientNumber, listener))
      }
    }

    readyFlag = true;

    if (!linkRawWireless.endHost(pollResponse))
      return Result::FAILURE;

    return Result::SUCCESS;
  }

  template <typename C>
  Result handshakeClient(u8 clientNumber, C listener) {
    ClientPacket handshakePackets[2] = {ClientPacket{}, ClientPacket{}};
    bool hasReceivedName = false;

    _LWMLOG_("new client: " + std::to_string(clientNumber));
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeAndValidate(
        clientNumber,
        [this](LinkRawWireless::ReceiveDataResponse& response) {
          return exchange({}, 0, 1, response);
        },
        [](ClientPacket packet) { return true; }, listener))
    // (initial client packet received)

    _LWMLOG_("handshake (1/2)...");
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeACKData(
        clientNumber,
        [](ClientPacket packet) {
          auto header = packet.header;
          return header.n == 2 && header.commState == CommState::STARTING;
        },
        listener))
    // (n = 2, commState = 1)

    _LWMLOG_("handshake (2/2)...");
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeACKData(
        clientNumber,
        [&handshakePackets](ClientPacket packet) {
          auto header = packet.header;
          bool isValid = header.n == 1 && header.phase == 0 &&
                         header.commState == CommState::COMMUNICATING;
          if (isValid)
            handshakePackets[0] = packet;
          return isValid;
        },
        listener))
    // (n = 1, commState = 2)

    _LWMLOG_("receiving name...");
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeACKData(
        clientNumber,
        [this, &handshakePackets, &hasReceivedName](ClientPacket packet) {
          auto header = packet.header;
          lastValidHeader = header;
          if (header.n == 1 && header.phase == 1 &&
              header.commState == CommState::COMMUNICATING) {
            handshakePackets[1] = packet;
            hasReceivedName = true;
          }
          return header.commState == CommState::OFF;
        },
        listener))
    // (commState = 0)

    _LWMLOG_("validating name...");
    if (!validateName(handshakePackets, hasReceivedName)) {
      _LWMLOG_("! bad payload");
      return Result::BAD_HANDSHAKE;
    }

    _LWMLOG_("draining queue...");
    bool hasFinished = false;
    while (!hasFinished) {
      if (listener(progress))
        return Result::CANCELED;

      LinkRawWireless::ReceiveDataResponse response;
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchange({}, 0, 1, response))
      auto childrenData = linkWirelessOpenSDK.getChildrenData(response);
      hasFinished = childrenData.responses[clientNumber].packetsSize == 0;
    }
    // (no more client packets)

    _LWMLOG_("client " + std::to_string(clientNumber) + " accepted");

    return Result::SUCCESS;
  }

  template <typename C>
  Result sendRomStartCommand(C listener) {
    for (u32 i = 0; i < progress.connectedClients; i++) {
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeNewData(
          i,
          linkWirelessOpenSDK.createServerBuffer(
              CMD_START, CMD_START_SIZE, {1, 0, CommState::STARTING}, 1 << i),
          listener))
    }

    return Result::SUCCESS;
  }

  template <typename C>
  Result sendRomBytes(const u8* rom, u32 romSize, C listener) {
    u8 firstPagePatch[LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER];
    generateFirstPagePatch(rom, firstPagePatch);
    progress.percentage = 0;

    LinkWirelessOpenSDK::MultiTransfer<MAX_INFLIGHT_PACKETS> multiTransfer(
        &linkWirelessOpenSDK);
    multiTransfer.configure(romSize, progress.connectedClients);

    while (!multiTransfer.hasFinished()) {
      if (listener(progress))
        return Result::CANCELED;

      LINK_WIRELESS_MULTIBOOT_TRY_SUB(ensureAllClientsAreStillAlive())

      auto sendBuffer = multiTransfer.createNextSendBuffer(
          multiTransfer.getCursor() == 0 ? (const u8*)firstPagePatch : rom);

      LinkRawWireless::ReceiveDataResponse response;
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchange(sendBuffer, response))

      u8 newPercentage = multiTransfer.processResponse(response);
      progress.percentage = newPercentage;
    }

    return Result::SUCCESS;
  }

  template <typename C>
  Result confirm(C listener) {
    _LWMLOG_("confirming (1/2)...");
    for (u32 i = 0; i < progress.connectedClients; i++) {
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(
          exchangeNewData(i,
                          linkWirelessOpenSDK.createServerBuffer(
                              {}, 0, {0, 0, CommState::ENDING}, 1 << i),
                          listener))
    }

    _LWMLOG_("confirming (2/2)...");
    for (u32 i = 0; i < FINAL_CONFIRMS; i++) {
      LinkRawWireless::ReceiveDataResponse response;
      auto sendBuffer = linkWirelessOpenSDK.createServerBuffer(
          {}, 0, {1, 0, CommState::OFF}, 0b1111);
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchange(sendBuffer, response))
    }

    return Result::SUCCESS;
  }

  template <typename C>
  Result exchangeNewData(u8 clientNumber, SendBuffer sendBuffer, C listener) {
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeAndValidate(
        clientNumber,
        [this, &sendBuffer](LinkRawWireless::ReceiveDataResponse& response) {
          return exchange(sendBuffer, response);
        },
        [&sendBuffer](ClientPacket packet) {
          auto header = packet.header;
          return header.isACK == 1 &&
                 header.sequence() == sendBuffer.header.sequence();
        },
        listener))

    return Result::SUCCESS;
  }

  template <typename V, typename C>
  Result exchangeACKData(u8 clientNumber, V validatePacket, C listener) {
    LINK_WIRELESS_MULTIBOOT_TRY_SUB(exchangeAndValidate(
        clientNumber,
        [this, clientNumber](LinkRawWireless::ReceiveDataResponse& response) {
          auto sendBuffer = linkWirelessOpenSDK.createServerACKBuffer(
              lastValidHeader, clientNumber);
          return exchange(sendBuffer, response);
        },
        validatePacket, listener))

    return Result::SUCCESS;
  }

  template <typename F, typename V, typename C>
  Result exchangeAndValidate(u8 clientNumber,
                             F sendAction,
                             V validatePacket,
                             C listener) {
    while (true) {
      if (listener(progress))
        return Result::CANCELED;

      LinkRawWireless::ReceiveDataResponse response;
      LINK_WIRELESS_MULTIBOOT_TRY_SUB(sendAction(response))
      auto childrenData = linkWirelessOpenSDK.getChildrenData(response);

      if (isDataValid(clientNumber, childrenData, lastValidHeader,
                      validatePacket))
        break;
    }

    return Result::SUCCESS;
  }

  Result exchange(SendBuffer& sendBuffer,
                  LinkRawWireless::ReceiveDataResponse& response) {
    return exchange(sendBuffer.data, sendBuffer.dataSize,
                    sendBuffer.totalByteCount, response);
  }

  Result exchange(const u32* data,
                  u32 dataSize,
                  u32 _bytes,
                  LinkRawWireless::ReceiveDataResponse& response) {
    LinkRawWireless::CommandResult remoteCommand;
    bool success = false;

    success =
        linkRawWireless.sendDataAndWait(data, dataSize, remoteCommand, _bytes);

    if (!success) {
      _LWMLOG_("! sendDataAndWait failed");
      return Result::FAILURE;
    }

    if (remoteCommand.commandId != LinkRawWireless::EVENT_DATA_AVAILABLE) {
      _LWMLOG_("! expected EVENT 0x28");
      _LWMLOG_("! but got " + toHex(remoteCommand.commandId));
      return Result::FAILURE;
    }

    if (remoteCommand.dataSize > 0 &&
        !areAllConnected(&remoteCommand, progress.connectedClients)) {
      _LWMLOG_("! client timeout");
      return Result::CLIENT_DISCONNECTED;
    }

    success = linkRawWireless.receiveData(response);

    if (!success) {
      _LWMLOG_("! receiveData failed");
      return Result::FAILURE;
    }

    return Result::SUCCESS;
  }

  Result ensureAllClientsAreStillAlive() {
    LinkRawWireless::SlotStatusResponse slotStatusResponse;
    if (!linkRawWireless.getSlotStatus(slotStatusResponse))
      return Result::FAILURE;

    if (slotStatusResponse.connectedClientsSize < progress.connectedClients)
      return Result::CLIENT_DISCONNECTED;

    return Result::SUCCESS;
  }

  Result finish(Result result, bool keepConnectionAlive = false) {
    if (result != Result::SUCCESS || !keepConnectionAlive)
      linkRawWireless.bye();
    linkRawWireless.deactivate();
    resetState();
    return result;
  }

  void resetState() {
    LINK_BARRIER;
    progress.state = State::STOPPED;
    progress.connectedClients = 0;
    progress.percentage = 0;
    progress.ready = &readyFlag;
    readyFlag = false;
    lastValidHeader = ClientHeader{};
    LINK_BARRIER;
  }

#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
  template <typename I>
  std::string toHex(I w, size_t hex_len = sizeof(I) << 1) {
    static const char* digits = "0123456789ABCDEF";
    std::string rc(hex_len, '0');
    for (size_t i = 0, j = (hex_len - 1) * 4; i < hex_len; ++i, j -= 4)
      rc[i] = digits[(w >> j) & 0x0F];
    return rc;
  }
#endif

  static bool validateName(ClientPacket* handshakePackets,
                           bool hasReceivedName) {
    for (u32 i = 0; i < 2; i++) {
      auto receivedPayload = handshakePackets[i].payload;
      auto expectedPayload = BOOTLOADER_HANDSHAKE[i];

      for (u32 j = 0; j < BOOTLOADER_HANDSHAKE_SIZE; j++) {
        if (!hasReceivedName || receivedPayload[j] != expectedPayload[j])
          return false;
      }
    }

    return true;
  }

  static void generateFirstPagePatch(const u8* rom, u8* firstPagePatch) {
    for (u32 i = 0; i < LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER; i++) {
      firstPagePatch[i] =
          i >= ROM_HEADER_PATCH_OFFSET &&
                  i < ROM_HEADER_PATCH_OFFSET + ROM_HEADER_PATCH_SIZE
              ? ROM_HEADER_PATCH[i - ROM_HEADER_PATCH_OFFSET]
              : rom[i];
    }
  }

  template <typename V>
  static bool isDataValid(u8 clientNumber,
                          ChildrenData& childrenData,
                          ClientHeader& lastReceivedHeader,
                          V validatePacket) {
    for (u32 i = 0; i < childrenData.responses[clientNumber].packetsSize; i++) {
      auto packet = childrenData.responses[clientNumber].packets[i];
      auto header = packet.header;
      if (validatePacket(packet)) {
        lastReceivedHeader = header;
        return true;
      }
    }
    return false;
  }

  static bool areAllConnected(LinkRawWireless::CommandResult* remoteCommand,
                              u32 connectedClients) {
    u8 expectedActiveChildren = 0;
    for (u32 i = 0; i < connectedClients; i++)
      expectedActiveChildren |= 1 << i;
    u8 activeChildren = (remoteCommand->data[0] >> 8) & expectedActiveChildren;

    return activeChildren == expectedActiveChildren;
  }

 public:
  /**
   * @brief [Asynchronous version] A Multiboot tool to send small ROMs from a
   * GBA to up to 4 slaves via GBA Wireless Adapter.
   */
  class Async : Link::AsyncMultiboot {
   private:
    using ServerHeader = LinkWirelessOpenSDK::ServerSDKHeader;

    static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024;
    static constexpr int FPS = 60;
    static constexpr int MAX_IRQ_TIMEOUT_FRAMES = FPS * 5;
    static constexpr int START_WAIT_FRAMES = 2;

   public:
#ifdef LINK_WIRELESS_MULTIBOOT_ENABLE_LOGGING
    Logger logger = [](std::string str){};
#endif

    using GeneralResult = Link::AsyncMultiboot::Result;

    enum class State {
      STOPPED = 0,
      INITIALIZING = 1,
      STARTING = 2,
      LISTENING = 3,
      HANDSHAKING_CLIENT_STEP1 = 4,
      HANDSHAKING_CLIENT_STEP2 = 5,
      HANDSHAKING_CLIENT_STEP3 = 6,
      HANDSHAKING_CLIENT_STEP4 = 7,
      HANDSHAKING_CLIENT_STEP5 = 8,
      ENDING_HOST = 9,
      SENDING_ROM_START_COMMAND = 10,
      ENSURING_CLIENTS_ALIVE = 11,
      SENDING_ROM_PART = 12,
      CONFIRMING_STEP1 = 13,
      CONFIRMING_STEP2 = 14,
    };

    enum class Result {
      NONE = -1,
      SUCCESS = 0,
      INVALID_SIZE = 1,
      INVALID_PLAYERS = 2,
      ADAPTER_NOT_DETECTED = 3,
      INIT_FAILURE = 4,
      BAD_HANDSHAKE = 5,
      CLIENT_DISCONNECTED = 6,
      FAILURE = 7,
      IRQ_TIMEOUT = 8
    };

    /**
     * @brief Constructs a new LinkWirelessMultiboot::Async object.
     * @param gameName Game name. Maximum `14` characters + null terminator.
     * @param userName User name. Maximum `8` characters + null terminator.
     * @param gameId `(0 ~ 0x7FFF)` The Game ID to be broadcasted.
     * @param players The number of consoles that will download the ROM.
     * Once this number of players is reached, the code will start transmitting
     * the ROM bytes, unless `waitForReadySignal` is `true`.
     * @param waitForReadySignal Whether the code should wait for a
     * `markReady()` call to start the actual transfer.
     * @param keepConnectionAlive If `true`, the adapter won't be reset after
     * a successful transfer, so users can continue the session using
     * `LinkWireless::restoreExistingConnection()`.
     * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers
     * *(50 = 3.052ms)*. It's the interval of Timer #`timerId`. Lower values
     * will transfer faster but also consume more CPU. Some audio players
     * require precise interrupt timing to avoid crashes! Use a minimum of 30.
     * @param timerId `(0~3)` GBA Timer to use for waiting.
     */
    explicit Async(
        const char* gameName = "",
        const char* userName = "",
        u16 gameId = LINK_RAW_WIRELESS_MAX_GAME_ID,
        u8 players = 5,
        bool waitForReadySignal = false,
        bool keepConnectionAlive = false,
        u16 interval = LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_INTERVAL,
        u8 timerId = LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_TIMER_ID)
        : multiTransfer(&linkWirelessOpenSDK) {
      config.gameName = gameName;
      config.userName = userName;
      config.gameId = gameId;
      config.players = players;
      config.waitForReadySignal = waitForReadySignal;
      config.keepConnectionAlive = keepConnectionAlive;
      config.interval = interval;
      config.timerId = timerId;
    }

    /**
     * @brief Sends the `rom`. Once completed, `getState()` should return
     * `LinkWirelessMultiboot::Async::State::STOPPED` and `getResult()` should
     * return `LinkWirelessMultiboot::Async::GeneralResult::SUCCESS`. Returns
     * `false` if there's a pending transfer or the data is invalid.
     * @param rom A pointer to ROM data.
     * @param romSize Size of the ROM in bytes. It must be a number between
     * `448` and `262144`. It's recommended to use a ROM size that is a multiple
     * of `16`, as this also ensures compatibility with Multiboot via Link
     * Cable.
     */
    bool sendRom(const u8* rom, u32 romSize) override {
      if (state != State::STOPPED)
        return false;

      if (romSize < LINK_WIRELESS_MULTIBOOT_MIN_ROM_SIZE ||
          romSize > LINK_WIRELESS_MULTIBOOT_MAX_ROM_SIZE) {
        result = Result::INVALID_SIZE;
        return false;
      }
      if (config.players < LINK_WIRELESS_MULTIBOOT_MIN_PLAYERS ||
          config.players > LINK_WIRELESS_MULTIBOOT_MAX_PLAYERS) {
        result = Result::INVALID_PLAYERS;
        return false;
      }

      stop();

      fixedData.rom = rom;
      fixedData.romSize = romSize;
      fixedData.gameName = config.gameName;
      fixedData.userName = config.userName;
      fixedData.gameId = config.gameId;
      fixedData.players = config.players;
      fixedData.waitForReadySignal = config.waitForReadySignal;
      fixedData.keepConnectionAlive = config.keepConnectionAlive;
      fixedData.timerId = config.timerId;
      generateFirstPagePatch(rom, fixedData.firstPagePatch);

      _LWMLOG_("starting...");
      state = State::INITIALIZING;
      if (!linkRawWireless.activate()) {
        _LWMLOG_("! adapter not detected");
        stop(Result::ADAPTER_NOT_DETECTED);
        return false;
      }
      _LWMLOG_("activated");

      if (!linkRawWireless.setup(fixedData.players, SETUP_TX) ||
          !linkRawWireless.broadcast(
              fixedData.gameName, fixedData.userName,
              fixedData.gameId | GAME_ID_MULTIBOOT_FLAG) ||
          !linkRawWireless.startHost(false)) {
        _LWMLOG_("! init failed");
        stop(Result::INIT_FAILURE);
        return false;
      }
      _LWMLOG_("host started");

      state = State::STARTING;

      return true;
    }

    /**
     * @brief Turns off the adapter and deactivates the library, canceling the
     * in-progress transfer, if any. It returns a boolean indicating whether
     * the transition to low consumption mode was successful.
     * \warning Never call this method inside an interrupt handler!
     */
    bool reset() override { return stop(); }

    /**
     * @brief Returns whether there's an active transfer or not.
     */
    [[nodiscard]] bool isSending() override { return state != State::STOPPED; }

    /**
     * @brief Returns the current state.
     */
    [[nodiscard]] State getState() { return state; }

    /**
     * @brief Returns the result of the last operation. After this
     * call, the result is cleared if `clear` is `true` (default behavior).
     * @param clear Whether it should clear the result or not.
     */
    Link::AsyncMultiboot::Result getResult(bool clear = true) override {
      auto detailedResult = getDetailedResult(clear);
      switch (detailedResult) {
        case Result::NONE:
          return Link::AsyncMultiboot::Result::NONE;
        case Result::SUCCESS:
          return Link::AsyncMultiboot::Result::SUCCESS;
        case Result::INVALID_SIZE:
        case Result::INVALID_PLAYERS:
          return Link::AsyncMultiboot::Result::INVALID_DATA;
        case Result::ADAPTER_NOT_DETECTED:
        case Result::INIT_FAILURE:
          return Link::AsyncMultiboot::Result::INIT_FAILED;
        default:
          return Link::AsyncMultiboot::Result::FAILURE;
      }
    }

    /**
     * @brief Returns the detailed result of the last operation. After this
     * call, the result is cleared if `clear` is `true` (default behavior).
     * @param clear Whether it should clear the result or not.
     */
    Result getDetailedResult(bool clear = true) {
      Result _result = result;
      if (clear)
        result = Result::NONE;
      return _result;
    }

    /**
     * @brief Returns the number of connected players (`1~5`).
     */
    [[nodiscard]] u8 playerCount() override {
      return 1 + dynamicData.connectedClients;
    }

    /**
     * @brief Returns the completion percentage (0~100).
     */
    [[nodiscard]] u8 getPercentage() override {
      if (state == State::STOPPED || fixedData.romSize == 0)
        return 0;

      return dynamicData.percentage;
    }

    /**
     * @brief Returns whether the ready mark is active or not.
     */
    [[nodiscard]] bool isReady() override { return dynamicData.ready; }

    /**
     * @brief Marks the transfer as ready.
     */
    void markReady() override {
      if (state == State::STOPPED)
        return;

      dynamicData.ready = true;
    }

    /**
     * @brief This method is called by the VBLANK interrupt handler.
     * \warning This is internal API!
     */
    void _onVBlank() {
      if (state == State::STOPPED)
        return;

      processNewFrame();
    }

    /**
     * @brief This method is called by the SERIAL interrupt handler.
     * \warning This is internal API!
     */
    void _onSerial() {
      if (state == State::STOPPED || interrupt)
        return;

#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
      interrupt = true;
#endif
      if (linkRawWireless._onSerial() > 0) {
        auto response = linkRawWireless._getAsyncCommandResultRef();
#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
        Link::_REG_IME = 1;
#endif
        processResponse(response);
      }
#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
      interrupt = false;
#endif
    }

    /**
     * @brief This method is called by the TIMER interrupt handler.
     * \warning This is internal API!
     */
    void _onTimer() {
      if (state != State::SENDING_ROM_PART || interrupt)
        return;

      state = State::ENSURING_CLIENTS_ALIVE;
      checkClientsAlive();
      stopTimer();
    }

   private:
    struct Config {
      const char* gameName;
      const char* userName;
      u16 gameId;
      u8 players;
      bool waitForReadySignal;
      bool keepConnectionAlive;
      u16 interval;
      u8 timerId;
    };

    enum class SendState { NOT_SENDING, SEND_AND_WAIT, RECEIVE };

    struct MultibootFixedData {
      const u8* rom = nullptr;
      u32 romSize = 0;
      const char* gameName = nullptr;
      const char* userName = nullptr;
      u16 gameId = 0;
      u8 players = 0;
      bool waitForReadySignal = false;
      bool keepConnectionAlive = false;
      u32 interval = LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_INTERVAL;
      u8 timerId = LINK_WIRELESS_MULTIBOOT_ASYNC_DEFAULT_TIMER_ID;

      u8 firstPagePatch[LinkWirelessOpenSDK::MAX_PAYLOAD_SERVER] = {};
    };

    struct HandshakeClientData {
      ClientPacket packets[2] = {ClientPacket{}, ClientPacket{}};
      bool didReceiveName = false;
    };

    struct MultibootDynamicData {
      u32 irqTimeout = 0;
      u32 wait = 0;
      u32 frameTransfers = 0;

      u8 currentClient = 0;
      HandshakeClientData handshakeClient = HandshakeClientData{};
      u32 percentage = 0;
      u32 confirmationTry = 0;

      ClientHeader lastReceivedHeader = ClientHeader{};
      ServerHeader lastSentHeader = ServerHeader{};

      bool ready = false;
      u8 connectedClients = 0;
    };

    LinkRawWireless linkRawWireless;
    LinkWirelessOpenSDK linkWirelessOpenSDK;
    Config config;
    SendState sendState;
    MultibootFixedData fixedData;
    MultibootDynamicData dynamicData;
    LinkWirelessOpenSDK::MultiTransfer<MAX_INFLIGHT_PACKETS> multiTransfer;
    volatile State state = State::STOPPED;
    volatile Result result = Result::NONE;
#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
    volatile bool interrupt = false;
#endif

    void processNewFrame() {
      dynamicData.frameTransfers = 0;
      dynamicData.irqTimeout++;
      if (dynamicData.irqTimeout >= MAX_IRQ_TIMEOUT_FRAMES) {
#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
        if (!interrupt)
          stop(Result::IRQ_TIMEOUT);
#endif
        return;
      }

      switch (state) {
        case State::STARTING: {
          dynamicData.wait++;
          if (dynamicData.wait >= START_WAIT_FRAMES) {
            state = State::LISTENING;
            startOrKeepListening();
          }
          break;
        }
        default: {
        }
      }
    }

    void processResponse(LinkRawWireless::CommandResult* response) {
      dynamicData.irqTimeout = 0;

      if (sendState == SendState::SEND_AND_WAIT) {
        if (!response->success ||
            response->commandId != LinkRawWireless::EVENT_DATA_AVAILABLE)
          return (void)stop(Result::FAILURE);

        if (response->dataSize > 0 &&
            !areAllConnected(response, dynamicData.connectedClients))
          return (void)stop(Result::CLIENT_DISCONNECTED);

        receiveAsync();
        return;
      } else if (sendState == SendState::RECEIVE) {
        if (!response->success)
          return (void)stop(Result::FAILURE);

        sendState = SendState::NOT_SENDING;
      }

      switch (state) {
        case State::LISTENING: {
          if (!response->success)
            return (void)stop(Result::FAILURE);

          LINK_BARRIER;
          u32 newConnectedClients = response->dataSize;
          linkRawWireless.sessionState.playerCount = 1 + newConnectedClients;
          LINK_BARRIER;

          if (newConnectedClients > dynamicData.connectedClients) {
            dynamicData.connectedClients = newConnectedClients;
            u8 lastClientNumber =
                (u8)Link::msB32(response->data[response->dataSize - 1]);
            _LWMLOG_("new client: " + std::to_string(lastClientNumber));

            state = State::HANDSHAKING_CLIENT_STEP1;
            startHandshakeWith(lastClientNumber);
          } else {
            state = State::STARTING;
            dynamicData.wait = START_WAIT_FRAMES - 1;
          }
          break;
        }
        case State::HANDSHAKING_CLIENT_STEP1: {
          u8 currentClient = dynamicData.currentClient;

          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isDataValid(currentClient, childrenData,
                           dynamicData.lastReceivedHeader,
                           [](ClientPacket packet) { return true; }))
            return (void)startHandshakeWith(currentClient);

          _LWMLOG_("handshake (1/2)...");
          state = State::HANDSHAKING_CLIENT_STEP2;
          sendACKData(currentClient);
          break;
        }
        case State::HANDSHAKING_CLIENT_STEP2: {
          u8 currentClient = dynamicData.currentClient;

          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isDataValid(currentClient, childrenData,
                           dynamicData.lastReceivedHeader,
                           [](ClientPacket packet) {
                             auto header = packet.header;
                             return header.n == 2 &&
                                    header.commState == CommState::STARTING;
                           }))
            return (void)sendACKData(currentClient);

          _LWMLOG_("handshake (2/2)...");
          state = State::HANDSHAKING_CLIENT_STEP3;
          sendACKData(currentClient);
          break;
        }
        case State::HANDSHAKING_CLIENT_STEP3: {
          u8 currentClient = dynamicData.currentClient;

          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isDataValid(
                  currentClient, childrenData, dynamicData.lastReceivedHeader,
                  [this](ClientPacket packet) {
                    auto header = packet.header;
                    bool isValid = header.n == 1 && header.phase == 0 &&
                                   header.commState == CommState::COMMUNICATING;
                    if (isValid)
                      dynamicData.handshakeClient.packets[0] = packet;
                    return isValid;
                  }))
            return (void)sendACKData(currentClient);

          _LWMLOG_("receiving name...");
          state = State::HANDSHAKING_CLIENT_STEP4;
          sendACKData(currentClient);
          break;
        }
        case State::HANDSHAKING_CLIENT_STEP4: {
          u8 currentClient = dynamicData.currentClient;

          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isDataValid(
                  currentClient, childrenData, dynamicData.lastReceivedHeader,
                  [this](ClientPacket packet) {
                    auto header = packet.header;
                    dynamicData.lastReceivedHeader = header;
                    if (header.n == 1 && header.phase == 1 &&
                        header.commState == CommState::COMMUNICATING) {
                      dynamicData.handshakeClient.packets[1] = packet;
                      dynamicData.handshakeClient.didReceiveName = true;
                    }
                    return header.commState == CommState::OFF;
                  }))
            return (void)sendACKData(currentClient);

          _LWMLOG_("validating name...");
          if (!validateName(dynamicData.handshakeClient.packets,
                            dynamicData.handshakeClient.didReceiveName)) {
            _LWMLOG_("! bad payload");
            return (void)stop(Result::BAD_HANDSHAKE);
          }

          _LWMLOG_("draining queue...");
          state = State::HANDSHAKING_CLIENT_STEP5;
          exchangeAsync({}, 0, 1);
          break;
        }
        case State::HANDSHAKING_CLIENT_STEP5: {
          u8 currentClient = dynamicData.currentClient;

          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          bool hasFinished =
              childrenData.responses[currentClient].packetsSize == 0;
          if (!hasFinished)
            return (void)exchangeAsync({}, 0, 1);

          _LWMLOG_("client " + std::to_string(currentClient) + " accepted");

          startOrKeepListening();
          break;
        }
        case State::ENDING_HOST: {
          if (!response->success)
            return (void)stop(Result::FAILURE);

          _LWMLOG_("rom start command...");
          dynamicData.currentClient = 0;
          state = State::SENDING_ROM_START_COMMAND;
          sendRomStartCommand();
          break;
        }
        case State::SENDING_ROM_START_COMMAND: {
          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isValidAcknowledge(childrenData))
            return (void)sendRomStartCommand();

          dynamicData.currentClient++;
          if (dynamicData.currentClient < dynamicData.connectedClients)
            return (void)sendRomStartCommand();

          _LWMLOG_("SENDING ROM!");
          state = State::ENSURING_CLIENTS_ALIVE;
          multiTransfer.configure(fixedData.romSize,
                                  dynamicData.connectedClients);
          checkClientsAlive();
          break;
        }
        case State::ENSURING_CLIENTS_ALIVE: {
          if (!response->success)
            return (void)stop(Result::FAILURE);

          if (response->dataSize - 1 < dynamicData.connectedClients)
            return (void)stop(Result::CLIENT_DISCONNECTED);

          state = State::SENDING_ROM_PART;
          sendRomPart();
          break;
        }
        case State::SENDING_ROM_PART: {
          LinkRawWireless::ReceiveDataResponse receiveDataResponse;
          if (!response->success || !linkRawWireless.getReceiveDataResponse(
                                        *response, receiveDataResponse))
            return (void)stop(Result::FAILURE);

          u8 newPercentage = multiTransfer.processResponse(receiveDataResponse);
          dynamicData.percentage = newPercentage;

          dynamicData.frameTransfers++;
          startTimer();
          break;
        }
        case State::CONFIRMING_STEP1: {
          ChildrenData childrenData;
          if (!parseResponse(response, childrenData))
            return (void)stop(Result::FAILURE);

          if (!isValidAcknowledge(childrenData))
            return (void)sendConfirmation1();

          dynamicData.currentClient++;
          if (dynamicData.currentClient < dynamicData.connectedClients)
            return (void)sendConfirmation1();

          _LWMLOG_("confirming (2/2)...");
          state = State::CONFIRMING_STEP2;
          dynamicData.confirmationTry = 0;
          sendConfirmation2();
          break;
        }
        case State::CONFIRMING_STEP2: {
          if (!response->success)
            return (void)stop(Result::FAILURE);

          dynamicData.confirmationTry++;
          if (dynamicData.confirmationTry < FINAL_CONFIRMS)
            return (void)sendConfirmation2();

          _LWMLOG_("SUCCESS!");
          stop(Result::SUCCESS);
          break;
        }
        default: {
        }
      }
    }

    void pollConnections() {
      sendCommandAsync(LinkRawWireless::COMMAND_POLL_CONNECTIONS);
    }

    void startHandshakeWith(u8 clientNumber) {
      dynamicData.currentClient = clientNumber;
      dynamicData.handshakeClient = HandshakeClientData{};

      exchangeAsync({}, 0, 1);
    };

    void startOrKeepListening() {
      if (linkRawWireless.playerCount() <= 1 ||
          (fixedData.waitForReadySignal && !dynamicData.ready) ||
          (linkRawWireless.playerCount() < fixedData.players &&
           !dynamicData.ready)) {
        state = State::LISTENING;
        return (void)pollConnections();
      }

      dynamicData.ready = true;

      _LWMLOG_("all players are connected");
      state = State::ENDING_HOST;
      sendCommandAsync(LinkRawWireless::COMMAND_END_HOST);
    }

    void sendRomStartCommand() {
      u8 clientNumber = dynamicData.currentClient;

      auto sendBuffer = linkWirelessOpenSDK.createServerBuffer(
          CMD_START, CMD_START_SIZE, {1, 0, CommState::STARTING},
          1 << clientNumber);
      sendNewData(sendBuffer);
    }

    void checkClientsAlive() {
      if (multiTransfer.hasFinished()) {
        _LWMLOG_("confirming (1/2)...");
        state = State::CONFIRMING_STEP1;
        dynamicData.currentClient = 0;
        sendConfirmation1();
        return;
      }

      sendCommandAsync(LinkRawWireless::COMMAND_SLOT_STATUS);
    }

    void sendRomPart() {
      auto sendBuffer = multiTransfer.createNextSendBuffer(
          multiTransfer.getCursor() == 0 ? (const u8*)fixedData.firstPagePatch
                                         : fixedData.rom);
      exchangeAsync(sendBuffer);
    }

    void sendConfirmation1() {
      auto sendBuffer = linkWirelessOpenSDK.createServerBuffer(
          {}, 0, {0, 0, CommState::ENDING}, 1 << dynamicData.currentClient);
      sendNewData(sendBuffer);
    }

    void sendConfirmation2() {
      auto sendBuffer = linkWirelessOpenSDK.createServerBuffer(
          {}, 0, {1, 0, CommState::OFF}, 0b1111);
      sendNewData(sendBuffer);
    }

    bool parseResponse(LinkRawWireless::CommandResult* response,
                       ChildrenData& childrenData) {
      LinkRawWireless::ReceiveDataResponse receiveDataResponse;
      if (!response->success || !linkRawWireless.getReceiveDataResponse(
                                    *response, receiveDataResponse))
        return false;
      childrenData = linkWirelessOpenSDK.getChildrenData(receiveDataResponse);
      return true;
    }

    bool isValidAcknowledge(ChildrenData& childrenData) {
      return isDataValid(
          dynamicData.currentClient, childrenData,
          dynamicData.lastReceivedHeader, [this](ClientPacket packet) {
            auto header = packet.header;
            return header.isACK == 1 &&
                   header.sequence() == dynamicData.lastSentHeader.sequence();
          });
    }

    void sendNewData(SendBuffer& sendBuffer) {
      dynamicData.lastSentHeader = sendBuffer.header;
      exchangeAsync(sendBuffer);
    }

    void sendACKData(u8 clientNumber) {
      auto ackBuffer = linkWirelessOpenSDK.createServerACKBuffer(
          dynamicData.lastReceivedHeader, clientNumber);
      exchangeAsync(ackBuffer);
    }

    void exchangeAsync(SendBuffer& sendBuffer) {
      exchangeAsync(sendBuffer.data, sendBuffer.dataSize,
                    sendBuffer.totalByteCount);
    }

    void exchangeAsync(const u32* data, u32 dataSize, u32 _bytes) {
      u32 rawData[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
      rawData[0] = linkRawWireless.getSendDataHeaderFor(_bytes);
      for (u32 i = 0; i < dataSize; i++)
        rawData[1 + i] = data[i];

      sendState = SendState::SEND_AND_WAIT;
      sendCommandAsync(LinkRawWireless::COMMAND_SEND_DATA_AND_WAIT, rawData,
                       1 + dataSize, true);
    }

    void receiveAsync() {
      sendState = SendState::RECEIVE;
      sendCommandAsync(LinkRawWireless::COMMAND_RECEIVE_DATA);
    }

    void stopTimer() {
      Link::_REG_TM[config.timerId].cnt =
          Link::_REG_TM[config.timerId].cnt & (~Link::_TM_ENABLE);
    }

    void startTimer() {
      Link::_REG_TM[config.timerId].start = -config.interval;
      Link::_REG_TM[config.timerId].cnt =
          Link::_TM_ENABLE | Link::_TM_IRQ | BASE_FREQUENCY;
    }

    void sendCommandAsync(u8 type,
                          const u32* params = {},
                          u16 length = 0,
                          bool invertsClock = false) {
#ifndef LINK_WIRELESS_MULTIBOOT_ASYNC_DISABLE_NESTED_IRQ
      Link::_REG_IME = 0;
#endif
      linkRawWireless.sendCommandAsync(type, params, length, invertsClock);
    }

    void resetState(Result newResult = Result::NONE) {
      LINK_BARRIER;
      state = State::STOPPED;
      result = newResult;
      sendState = SendState::NOT_SENDING;
      fixedData = MultibootFixedData{};
      dynamicData = MultibootDynamicData{};
      LINK_BARRIER;
    }

    bool stop(Result newResult = Result::NONE) {
      bool keepConnectionAlive = fixedData.keepConnectionAlive;
      resetState(newResult);
      stopTimer();

      bool success = true;
      if (newResult != Result::SUCCESS || !keepConnectionAlive)
        success = linkRawWireless.bye();
      linkRawWireless.deactivate();
      return success;
    }
  };
};

extern LinkWirelessMultiboot* linkWirelessMultiboot;
extern LinkWirelessMultiboot::Async* linkWirelessMultibootAsync;

/**
 * @brief VBLANK interrupt handler.
 */
inline void LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_VBLANK() {
  linkWirelessMultibootAsync->_onVBlank();
}

/**
 * @brief SERIAL interrupt handler.
 */
inline void LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_SERIAL() {
  linkWirelessMultibootAsync->_onSerial();
}

/**
 * @brief TIMER interrupt handler.
 */
inline void LINK_WIRELESS_MULTIBOOT_ASYNC_ISR_TIMER() {
  linkWirelessMultibootAsync->_onTimer();
}

#undef _LWMLOG_

#endif  // LINK_WIRELESS_MULTIBOOT_H
