#ifndef LINK_WIRELESS_H
#define LINK_WIRELESS_H

// --------------------------------------------------------------------------
// A high level driver for the GBA Wireless Adapter.
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
//       LinkWireless* linkWireless = new LinkWireless();
// - 2) Add the required interrupt service routines: (*)
//       interrupt_init();
//       interrupt_add(INTR_VBLANK, LINK_WIRELESS_ISR_VBLANK);
//       interrupt_add(INTR_SERIAL, LINK_WIRELESS_ISR_SERIAL);
//       interrupt_add(INTR_TIMER3, LINK_WIRELESS_ISR_TIMER);
// - 3) Initialize the library with:
//       linkWireless->activate();
// - 4) Start a server:
//       linkWireless->serve();
//
//       // `getState()` should return SERVING now...
//       // `currentPlayerId()` should return 0
//       // `playerCount()` should return the number of active consoles
// - 5) Connect to a server:
//       LinkWireless::Server servers[LINK_WIRELESS_MAX_SERVERS];
//       u32 serverCount;
//       linkWireless->getServers(servers, serverCount);
//       if (serverCount == 0) return;
//
//       linkWireless->connect(servers[0].id);
//       while (linkWireless->getState() == LinkWireless::State::CONNECTING)
//         linkWireless->keepConnecting();
//
//       // `getState()` should return CONNECTED now...
//       // `currentPlayerId()` should return 1, 2, 3, or 4 (the host is 0)
//       // `playerCount()` should return the number of active consoles
// - 6) Send data:
//       linkWireless->send(0x1234);
// - 7) Receive data:
//       LinkWireless::Message messages[LINK_WIRELESS_QUEUE_SIZE];
//       u32 receivedCount;
//       linkWireless->receive(messages, receivedCount);
// - 8) Disconnect:
//       linkWireless->activate();
//       // (resets the adapter)
// --------------------------------------------------------------------------
// (*) libtonc's interrupt handler sometimes ignores interrupts due to a bug.
//     That causes packet loss. You REALLY want to use libugba's instead.
//     (see examples)
// --------------------------------------------------------------------------

#ifndef LINK_DEVELOPMENT
#pragma GCC system_header
#endif

#include "_link_common.hpp"

#include "LinkRawWireless.hpp"

#ifndef LINK_WIRELESS_QUEUE_SIZE
/**
 * @brief Buffer size (how many incoming and outgoing messages the queues can
 * store at max **per player**). The default value is `30`, which seems fine for
 * most games.
 * \warning This affects how much memory is allocated. With the default value,
 * it's around `480` bytes. There's a double-buffered incoming queue and a
 * double-buffered outgoing queue (to avoid data races).
 * \warning You can approximate the usage with `LINK_WIRELESS_QUEUE_SIZE * 16`.
 */
#define LINK_WIRELESS_QUEUE_SIZE 30
#endif

#ifndef LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH
/**
 * @brief Max server transfer length per timer tick. Must be in the range
 * `[6;21]`. The default value is `11`. Higher values will use the bandwidth
 * more efficiently but also consume more CPU!
 * \warning This is measured in words (1 message = 1 halfword). One word is used
 * as a header, so a max transfer length of 11 could transfer up to 20 messages.
 */
#define LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH 11
#endif

#ifndef LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH
/**
 * @brief Max client transfer length per timer tick. Must be in the range
 * `[2;4]`. The default value is `4`. Changing this is not recommended, it's
 * already too low.
 * \warning This is measured in words (1 message = 1 halfword). One halfword is
 * used as a header, so a max transfer length of 4 could transfer up to 7
 * messages.
 */
#define LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH 4
#endif

#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM
/**
 * @brief Put Interrupt Service Routines (ISR) in IWRAM (uncomment to enable).
 * This can significantly improve performance due to its faster access, but it's
 * disabled by default to conserve IWRAM space, which is limited.
 * \warning If you enable this, make sure that `lib/iwram_code/LinkWireless.cpp`
 * gets compiled! For example, in a Makefile-based project, verify that the
 * directory is in your `SRCDIRS` list.
 */
// #define LINK_WIRELESS_PUT_ISR_IN_IWRAM
#endif

#ifndef LINK_WIRELESS_ENABLE_NESTED_IRQ
/**
 * @brief Allow LINK_WIRELESS_ISR_* functions to be interrupted (uncomment to
 * enable).
 * This can be useful, for example, if your audio engine requires calling a
 * VBlank handler with precise timing.
 */
// #define LINK_WIRELESS_ENABLE_NESTED_IRQ
#endif

// --- LINK_WIRELESS_PUT_ISR_IN_IWRAM knobs ---
#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL
#define LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL 1
#endif
#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER
#define LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER 1
#endif
#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL_LEVEL
#define LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL_LEVEL "-Ofast"
#endif
#ifndef LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER_LEVEL
#define LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER_LEVEL "-Ofast"
#endif
//---

LINK_VERSION_TAG LINK_WIRELESS_VERSION = "vLinkWireless/v8.0.0";

#define LINK_WIRELESS_MAX_PLAYERS LINK_RAW_WIRELESS_MAX_PLAYERS
#define LINK_WIRELESS_MIN_PLAYERS 2
#define LINK_WIRELESS_MAX_SERVERS LINK_RAW_WIRELESS_MAX_SERVERS
#define LINK_WIRELESS_MAX_GAME_ID 0x7FFF
#define LINK_WIRELESS_MAX_GAME_NAME_LENGTH 14
#define LINK_WIRELESS_MAX_USER_NAME_LENGTH 8
#define LINK_WIRELESS_DEFAULT_TIMEOUT 10
#define LINK_WIRELESS_DEFAULT_INTERVAL 75
#define LINK_WIRELESS_DEFAULT_SEND_TIMER_ID 3

#define LINK_WIRELESS_RESET_IF_NEEDED                   \
  if (!isEnabled)                                       \
    return false;                                       \
  if (linkRawWireless.getState() == State::NEEDS_RESET) \
    if (!reset())                                       \
      return false;

#ifdef LINK_WIRELESS_PUT_ISR_IN_IWRAM
#if LINK_WIRELESS_PUT_ISR_IN_IWRAM_SERIAL == 1
#define LINK_WIRELESS_SERIAL_ISR LINK_INLINE
#else
#define LINK_WIRELESS_SERIAL_ISR
#endif
#if LINK_WIRELESS_PUT_ISR_IN_IWRAM_TIMER == 1
#define LINK_WIRELESS_TIMER_ISR LINK_INLINE
#else
#define LINK_WIRELESS_TIMER_ISR
#endif

#define LINK_WIRELESS_ISR_FUNC(name, params, args, body) \
  void name params;                                      \
  LINK_INLINE void _##name params body
#else
#define LINK_WIRELESS_SERIAL_ISR
#define LINK_WIRELESS_TIMER_ISR
#define LINK_WIRELESS_ISR_FUNC(name, params, args, body) \
  void name params {                                     \
    _##name args;                                        \
  }                                                      \
  LINK_INLINE void _##name params body
#endif

/**
 * @brief A high level driver for the GBA Wireless Adapter.
 */
class LinkWireless {
 private:
  using u32 = Link::u32;
  using u16 = Link::u16;
  using u8 = Link::u8;
  using vu8 = Link::vu8;

  static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024;
  static constexpr int BROADCAST_SEARCH_WAIT_FRAMES = 60;
  static constexpr int MAX_PACKET_IDS_SERVER = 1 << 6;
  static constexpr int MAX_PACKET_IDS_CLIENT = 1 << 4;
  static constexpr int MAX_INFLIGHT_PACKETS_SERVER =
      MAX_PACKET_IDS_SERVER / 2 - 1;
  static constexpr int MAX_INFLIGHT_PACKETS_CLIENT =
      MAX_PACKET_IDS_CLIENT / 2 - 1;
  static constexpr int NO_ID_ASSIGNED_YET = 0xFF;
  static constexpr u32 NO_ACK_RECEIVED_YET = 0xFFFFFFFF;
  static constexpr int HAS_FIRST_MSG_MASK = 0b10000;
  static constexpr int MAX_PLAYER_BITMAP_ENTRIES = 5;
  static constexpr int PLAYER_ID_BITS = 3;
  static constexpr int PLAYER_ID_MASK = 0b111;
  static constexpr int BIT_HAS_MORE = 15;

 public:
// #define LINK_WIRELESS_PROFILING_ENABLED
#ifdef LINK_WIRELESS_PROFILING_ENABLED
  u32 vblankTime = 0;
  u32 serialTime = 0;
  u32 timerTime = 0;
  u32 vblankIRQs = 0;
  u32 serialIRQs = 0;
  u32 timerIRQs = 0;
#endif

  using State = LinkRawWireless::State;
  using SignalLevelResponse = LinkRawWireless::SignalLevelResponse;

  enum class Error {
    // User errors
    NONE = 0,
    WRONG_STATE = 1,
    GAME_NAME_TOO_LONG = 2,
    USER_NAME_TOO_LONG = 3,
    BUFFER_IS_FULL = 4,
    // Communication errors
    COMMAND_FAILED = 5,
    CONNECTION_FAILED = 6,
    SEND_DATA_FAILED = 7,
    RECEIVE_DATA_FAILED = 8,
    ACKNOWLEDGE_FAILED = 9,
    TIMEOUT = 10,
    REMOTE_TIMEOUT = 11,
    BUSY_TRY_AGAIN = 12,
  };

  struct Message {
    u16 data = 0;
    u8 playerId = 0;
    u8 packetId = NO_ID_ASSIGNED_YET;
  };

  struct Server {
    u16 id = 0;
    u16 gameId;
    char gameName[LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1];
    char userName[LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1];
    u8 currentPlayerCount;

    bool isFull() { return currentPlayerCount == 0; }
  };

  /**
   * @brief Constructs a new LinkWireless object.
   * @param forwarding If `true`, the server forwards all messages to the
   * clients. Otherwise, clients only see messages sent from the server
   * (ignoring other peers).
   * @param retransmission If `true`, the library handles retransmission for
   * you, so there should be no packet loss.
   * @param maxPlayers `(2~5)` Maximum number of allowed players.
   * @param timeout Number of *frames* without receiving *any* data to reset the
   * connection.
   * @param interval Number of *1024-cycle ticks* (61.04μs) between transfers
   * *(75 = 4.578ms)*. It's the interval of Timer #`sendTimerId`. Lower values
   * will transfer faster but also consume more CPU.
   * @param sendTimerId `(0~3)` GBA Timer to use for sending.
   * \warning You can use `Link::perFrame(...)` to convert from *packets per
   * frame* to *interval values*.
   */
  explicit LinkWireless(bool forwarding = true,
                        bool retransmission = true,
                        u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS,
                        u32 timeout = LINK_WIRELESS_DEFAULT_TIMEOUT,
                        u16 interval = LINK_WIRELESS_DEFAULT_INTERVAL,
                        u8 sendTimerId = LINK_WIRELESS_DEFAULT_SEND_TIMER_ID) {
    config.forwarding = forwarding;
    config.retransmission = retransmission;
    config.maxPlayers = maxPlayers;
    config.timeout = timeout;
    config.interval = interval;
    config.sendTimerId = sendTimerId;
  }

  /**
   * @brief Returns whether the library is active or not.
   */
  [[nodiscard]] bool isActive() { return isEnabled; }

  /**
   * @brief Activates the library. When an adapter is connected, it changes the
   * state to `AUTHENTICATED`. It can also be used to disconnect or reset the
   * adapter.
   */
  bool activate() {
    LINK_READ_TAG(LINK_WIRELESS_VERSION);
    static_assert(LINK_WIRELESS_QUEUE_SIZE >= 1);
    static_assert(LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH >= 6 &&
                  LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH <= 21);
    static_assert(LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH >= 2 &&
                  LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH <= 4);

    lastError = Error::NONE;
    isEnabled = false;

    LINK_BARRIER;
    bool success = reset();
    LINK_BARRIER;

    isEnabled = true;
    return success;
  }

  /**
   * @brief Restores the state from an existing connection on the Wireless
   * Adapter hardware. This is useful, for example, after a fresh launch of a
   * Multiboot game, to synchronize the library with the current state and
   * avoid a reconnection. Returns whether the restoration was successful.
   * On success, the state should be either `SERVING` or `CONNECTED`.
   * \warning This should be used as a replacement for `activate()`.
   */
  bool restoreExistingConnection() {
    isEnabled = false;

    resetState();
    stopTimer();
    startTimer();

    if (!linkRawWireless.restoreExistingConnection() ||
        linkRawWireless.sessionState.playerCount > config.maxPlayers) {
      deactivate();
      return false;
    }

    isEnabled = true;
    return true;
  }

  /**
   * @brief Puts the adapter into a low consumption mode and then deactivates
   * the library. It returns a boolean indicating whether the transition to low
   * consumption mode was successful.
   * @param turnOff Whether the library should put the adapter in the low
   * consumption mode or not before deactivation. Defaults to `true`.
   */
  bool deactivate(bool turnOff = true) {
    bool success = true;

    if (turnOff)
      success = activate() && linkRawWireless.bye();

    lastError = Error::NONE;
    isEnabled = false;
    resetState();
    stop();

    return success;
  }

  /**
   * @brief Starts broadcasting a server and changes the state to `SERVING`. You
   * can optionally provide data that games will be able to read. If the adapter
   * is already serving, this method only updates the broadcast data.
   * @param gameName Game name. Maximum `14` characters + null terminator.
   * @param userName User name. Maximum `8` characters + null terminator.
   * @param gameId `(0 ~ 0x7FFF)` Game ID.
   * \warning Updating broadcast data while serving can fail if the adapter is
   * busy. In that case, this will return `false` and `getLastError()` will be
   * `BUSY_TRY_AGAIN`.
   */
  bool serve(const char* gameName = "",
             const char* userName = "",
             u16 gameId = LINK_WIRELESS_MAX_GAME_ID) {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::AUTHENTICATED &&
        linkRawWireless.getState() != State::SERVING)
      return badRequest(Error::WRONG_STATE);
    if (Link::strlen(gameName) > LINK_WIRELESS_MAX_GAME_NAME_LENGTH)
      return badRequest(Error::GAME_NAME_TOO_LONG);
    if (Link::strlen(userName) > LINK_WIRELESS_MAX_USER_NAME_LENGTH)
      return badRequest(Error::USER_NAME_TOO_LONG);

    isSendingSyncCommand = true;
    if (isAsyncCommandActive())
      return badRequest(Error::BUSY_TRY_AGAIN);

    if (linkRawWireless.getState() != State::SERVING) {
      if (!setup(config.maxPlayers))
        return abort(Error::COMMAND_FAILED);
    }

    bool success = linkRawWireless.broadcast(gameName, userName, gameId, false);

    if (linkRawWireless.getState() != State::SERVING)
      success = success && linkRawWireless.startHost(false);

    if (!success)
      return abort(Error::COMMAND_FAILED);

    LINK_BARRIER;
    isSendingSyncCommand = false;
    LINK_BARRIER;

    return true;
  }

  /**
   * @brief Closes the server while keeping the session active, to prevent new
   * users from joining the room.
   * \warning This action can fail if the adapter is busy. In that case,
   * this will return `false` and `getLastError()` will be `BUSY_TRY_AGAIN`.
   */
  bool closeServer() {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::SERVING ||
        linkRawWireless.sessionState.isServerClosed)
      return badRequest(Error::WRONG_STATE);

    isSendingSyncCommand = true;
    if (isAsyncCommandActive())
      return badRequest(Error::BUSY_TRY_AGAIN);

    LinkRawWireless::PollConnectionsResponse response;
    bool success = linkRawWireless.endHost(response);

    if (!success)
      return abort(Error::COMMAND_FAILED);

    LINK_BARRIER;
    isSendingSyncCommand = false;
    LINK_BARRIER;

    return true;
  }

  /**
   * @brief Retrieves the signal level of each player (0-255). For hosts, the
   * array will contain the signal level of each client in indexes 1-4. For
   * clients, it will only include the index corresponding to the
   * `currentPlayerId()`.
   * @param response A structure that will be filled with the signal levels.
   * \warning For clients, this action can fail if the adapter is busy. In that
   * case, this will return `false` and `getLastError()` will be
   * `BUSY_TRY_AGAIN`. For hosts, you already have this data, so it's free!
   */
  bool getSignalLevel(SignalLevelResponse& response) {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (!isSessionActive())
      return badRequest(Error::WRONG_STATE);

    if (linkRawWireless.getState() == LinkRawWireless::State::SERVING) {
      for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++)
        response.signalLevels[i] = sessionState.signalLevel.level[i];
      return true;
    }

    isSendingSyncCommand = true;
    if (isAsyncCommandActive())
      return badRequest(Error::BUSY_TRY_AGAIN);

    bool success = linkRawWireless.getSignalLevel(response);

    if (!success)
      return abort(Error::COMMAND_FAILED);

    LINK_BARRIER;
    isSendingSyncCommand = false;
    LINK_BARRIER;

    return true;
  }

  /**
   * @brief Fills the `servers` array with all the currently broadcasting
   * servers.
   * @param servers The array to be filled with data.
   * @param serverCount The number to be filled with the number of found
   * servers.
   * \warning This action takes 1 second to complete.
   * \warning For an async version, see `getServersAsyncStart()`.
   */
  bool getServers(Server servers[], u32& serverCount) {
    return getServers(servers, serverCount, []() {});
  }

  /**
   * @brief Fills the `servers` array with all the currently broadcasting
   * servers.
   * @param servers The array to be filled with data.
   * @param serverCount The number to be filled with the number of found
   * servers.
   * @param onWait A function which will be invoked each time VBlank starts.
   * \warning This action takes 1 second to complete.
   * \warning For an async version, see `getServersAsyncStart()`.
   */
  template <typename F>
  bool getServers(Server servers[], u32& serverCount, F onWait) {
    serverCount = 0;

    if (!getServersAsyncStart())
      return false;

    waitVBlanks(BROADCAST_SEARCH_WAIT_FRAMES, onWait);

    if (!getServersAsyncEnd(servers, serverCount))
      return false;

    return true;
  }

  /**
   * @brief Starts looking for broadcasting servers and changes the state to
   * `SEARCHING`. After this, call `getServersAsyncEnd(...)` 1 second later.
   */
  bool getServersAsyncStart() {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::AUTHENTICATED)
      return badRequest(Error::WRONG_STATE);

    bool success = linkRawWireless.broadcastReadStart();

    if (!success)
      return abort(Error::COMMAND_FAILED);

    return true;
  }

  /**
   * @brief Fills the `servers` array with all the currently broadcasting
   * servers. Changes the state to `AUTHENTICATED` again.
   * @param servers The array to be filled with data.
   * @param serverCount The number to be filled with the number of found
   * servers.
   */
  bool getServersAsyncEnd(Server servers[], u32& serverCount) {
    serverCount = 0;

    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::SEARCHING)
      return badRequest(Error::WRONG_STATE);

    LinkRawWireless::BroadcastReadPollResponse response;
    bool success1 = linkRawWireless.broadcastReadPoll(response);

    if (!success1)
      return abort(Error::COMMAND_FAILED);

    bool success2 = linkRawWireless.broadcastReadEnd();

    if (!success2)
      return abort(Error::COMMAND_FAILED);

    auto foundServers = response.servers;
    for (u32 i = 0; i < response.serversSize; i++) {
      Server server;
      server.id = foundServers[i].id;
      server.gameId = foundServers[i].gameId;
      for (u32 j = 0; j < LINK_WIRELESS_MAX_GAME_NAME_LENGTH + 1; j++)
        server.gameName[j] = foundServers[i].gameName[j];
      for (u32 j = 0; j < LINK_WIRELESS_MAX_USER_NAME_LENGTH + 1; j++)
        server.userName[j] = foundServers[i].userName[j];
      u8 nextClientNumber = foundServers[i].nextClientNumber;
      server.currentPlayerCount =
          nextClientNumber == 0xFF ? 0 : 1 + nextClientNumber;
      servers[i] = server;
    }
    serverCount = response.serversSize;

    return true;
  }

  /**
   * @brief Starts a connection with `serverId` and changes the state to
   * `CONNECTING`.
   * @param serverId Device ID of the server.
   */
  bool connect(u16 serverId) {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::AUTHENTICATED)
      return badRequest(Error::WRONG_STATE);

    bool success = linkRawWireless.connect(serverId);

    if (!success)
      return abort(Error::COMMAND_FAILED);

    return true;
  }

  /**
   * @brief When connecting, this needs to be called until the state is
   * `CONNECTED`. It assigns a player ID. Keep in mind that `isConnected()` and
   * `playerCount()` won't be updated until the first message from the server
   * arrives.
   */
  bool keepConnecting() {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (linkRawWireless.getState() != State::CONNECTING)
      return badRequest(Error::WRONG_STATE);

    LinkRawWireless::ConnectionStatus response;
    bool success1 = linkRawWireless.keepConnecting(response);

    if (!success1)
      return abort(Error::COMMAND_FAILED);

    if (response.phase == LinkRawWireless::ConnectionPhase::STILL_CONNECTING)
      return true;
    else if (response.phase == LinkRawWireless::ConnectionPhase::ERROR)
      return abort(Error::COMMAND_FAILED);

    auto success2 = linkRawWireless.finishConnection();
    if (!success2)
      return abort(Error::COMMAND_FAILED);

    return true;
  }

  /**
   * @brief Returns whether a `send(...)` call would fail due to the queue being
   * full or not.
   */
  bool canSend() { return !sessionState.newOutgoingMessages.isFull(); }

  /**
   * @brief Enqueues `data` to be sent to other nodes.
   * @param data The value to be sent.
   */
  bool send(u16 data) {
    LINK_WIRELESS_RESET_IF_NEEDED
    if (!isSessionActive())
      return badRequest(Error::WRONG_STATE);

    if (!canSend()) {
      lastError = Error::BUFFER_IS_FULL;
      return false;
    }

    Message message;
    message.playerId = linkRawWireless.sessionState.currentPlayerId;
    message.data = data;

    sessionState.newOutgoingMessages.syncPush(message);

    return true;
  }

  /**
   * @brief Fills the `messages` array with incoming messages.
   * @param messages The array to be filled with data.
   * @param receivedCount The number to be filled with the number of received
   * messages.
   */
  bool receive(Message messages[], u32& receivedCount) {
    receivedCount = 0;

    if (!isSessionActive())
      return false;

    LINK_BARRIER;
    sessionState.incomingMessages.startReading();
    LINK_BARRIER;

    receivedCount = 0;
    while (!sessionState.incomingMessages.isEmpty()) {
      auto message = sessionState.incomingMessages.pop();
      messages[receivedCount] = message;
      receivedCount++;
    }

    LINK_BARRIER;
    sessionState.incomingMessages.stopReading();
    LINK_BARRIER;

    return true;
  }

  /**
   * @brief Returns the current state.
   * @return One of the enum values from `State`.
   */
  [[nodiscard]] State getState() { return linkRawWireless.getState(); }

  /**
   * @brief Returns `true` if the player count is higher than `1`.
   */
  [[nodiscard]] bool isConnected() {
    return linkRawWireless.sessionState.playerCount > 1;
  }

  /**
   * @brief Returns `true` if the state is `SERVING` or `CONNECTED`.
   */
  [[nodiscard]] bool isSessionActive() {
    return linkRawWireless.getState() == State::SERVING ||
           linkRawWireless.getState() == State::CONNECTED;
  }

  /**
   * @brief Returns `true` if the server was closed with `closeServer()`.
   */
  [[nodiscard]] bool isServerClosed() {
    return linkRawWireless.sessionState.isServerClosed;
  }

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

  /**
   * @brief Returns the current player ID (`0~4`).
   */
  [[nodiscard]] u8 currentPlayerId() {
    return linkRawWireless.sessionState.currentPlayerId;
  }

  /**
   * @brief Returns whether the internal queue lost messages at some point due
   * to being full. This can happen if your queue size is too low, if you
   * receive too much data without calling `receive(...)` enough times, or if
   * excessive `receive(...)` calls prevent the ISR from copying data. After
   * this call, the overflow flag is cleared if `clear` is `true` (default
   * behavior).
   */
  bool didQueueOverflow(bool clear = true) {
    bool overflowReceive = sessionState.newIncomingMessages.overflow;
    bool overflowForwardedMessage = sessionState.outgoingMessages.overflow;
    if (clear) {
      sessionState.newIncomingMessages.overflow = false;
      sessionState.outgoingMessages.overflow = false;
    }
    return overflowReceive || overflowForwardedMessage;
  }

  /**
   * @brief Resets other players' timeout count to `0`.
   * \warning Call this if you changed `config.timeout`.
   */
  void resetTimeout() {
    if (!isEnabled)
      return;

    LINK_BARRIER;
    sessionState.isResetTimeoutPending = true;
    LINK_BARRIER;
  }

  /**
   * @brief Restarts the send timer without disconnecting.
   * \warning Call this if you changed `config.interval`.
   */
  void resetTimer() {
    if (!isEnabled)
      return;

    stopTimer();
    startTimer();
  }

  /**
   * @brief If one of the other methods returns `false`, you can inspect this to
   * know the cause. After this call, the last error is cleared if `clear` is
   * `true` (default behavior).
   * @param clear Whether it should clear the error or not.
   */
  Error getLastError(bool clear = true) {
    Error error = lastError;
    if (clear)
      lastError = Error::NONE;
    return error;
  }

  /**
   * @brief Returns the number of total outgoing messages.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _getPendingCount() {
    return sessionState.outgoingMessages.size();
  }

  /**
   * @brief Returns the number of inflight outgoing messages.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _getInflightCount() { return sessionState.inflightCount; }

  /**
   * @brief Returns the number of forwarded outgoing messages.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _getForwardedCount() { return sessionState.forwardedCount; }

  /**
   * @brief Returns the last packet ID.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _lastPacketId() { return sessionState.lastPacketId; }

  /**
   * @brief Returns the last ACK received from player ID 1.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _lastAckFromClient1() {
    return sessionState.lastAckFromClients[1];
  }

  /**
   * @brief Returns the last packet ID received from player ID 1.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _lastPacketIdFromClient1() {
    return sessionState.lastPacketIdFromClients[1];
  }

  /**
   * @brief Returns the last ACK received from the server.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _lastAckFromServer() {
    return sessionState.lastAckFromServer;
  }

  /**
   * @brief Returns the last packet ID received from the server.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _lastPacketIdFromServer() {
    return sessionState.lastPacketIdFromServer;
  }

  /**
   * @brief Returns the next pending packet ID.
   * \warning This is internal API!
   */
  [[nodiscard]] u32 _nextPendingPacketId() {
    return sessionState.outgoingMessages.isEmpty()
               ? 0
               : sessionState.outgoingMessages.peek().packetId;
  }
#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

  /**
   * @brief This method is called by the VBLANK interrupt handler.
   * \warning This is internal API!
   */
#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
  LINK_NOINLINE void _onVBlank() {
#else
  void _onVBlank() {
#endif
    if (!isEnabled)
      return;

#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
    if (interrupt) {
      pendingVBlank = true;
      return;
    }
#endif

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    profileStart();
#endif

    if (!isSessionActive())
      return;

    if (sessionState.isResetTimeoutPending) {
      sessionState.recvTimeout = 0;
      for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++)
        sessionState.msgTimeouts[i] = 0;
      sessionState.isResetTimeoutPending = false;
    }

    if (isConnected() && !sessionState.recvFlag)
      sessionState.recvTimeout++;
    if (sessionState.recvTimeout >= config.timeout)
      return (void)abort(Error::TIMEOUT);

    if (!checkRemoteTimeouts())
      return (void)abort(Error::REMOTE_TIMEOUT);

    sessionState.recvFlag = false;
    sessionState.signalLevelCalled = false;

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    vblankTime += profileStop();
    vblankIRQs++;
#endif
  }

  /**
   * @brief This method is called by the SERIAL interrupt handler.
   * \warning This is internal API!
   */
  LINK_WIRELESS_ISR_FUNC(_onSerial, (), (), {
#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
    interrupt = true;
    LINK_BARRIER;
    // (nested interrupts are enabled by LinkRawWireless::_onSerial(...))
#endif

    ___onSerial();

#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
    irqEnd();
#endif
  })

  /**
   * @brief This method is called by the TIMER interrupt handler.
   * \warning This is internal API!
   */
  LINK_WIRELESS_ISR_FUNC(_onTimer, (), (), {
#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
    if (interrupt)
      return;

    interrupt = true;
    LINK_BARRIER;
    Link::_REG_IME = 1;
#endif

    ___onTimer();

#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
    irqEnd();
#endif
  })

  struct Config {
    bool forwarding;
    bool retransmission;
    u8 maxPlayers;
    u32 timeout;   // can be changed in realtime, but call `resetTimeout()`
    u16 interval;  // can be changed in realtime, but call `resetTimer()`
    u8 sendTimerId;
  };

  /**
   * @brief LinkWireless configuration.
   * \warning `deactivate()` first, change the config, and `activate()` again!
   */
  Config config;

#ifndef LINK_WIRELESS_DEBUG_MODE
 private:
#endif
  using MessageQueue = Link::Queue<Message, LINK_WIRELESS_QUEUE_SIZE>;

  struct SignalLevel {
    vu8 level[LINK_WIRELESS_MAX_PLAYERS] = {};
  };

  struct SessionState {
    MessageQueue incomingMessages;     // read by user, write by irq&user
    MessageQueue outgoingMessages;     // read and write by irq
    MessageQueue newIncomingMessages;  // read and write by irq
    MessageQueue newOutgoingMessages;  // read by irq, write by user&irq
    SignalLevel signalLevel;           // write by irq, read by any

    u32 recvTimeout = 0;                         // (~= LinkCable::IRQTimeout)
    u32 msgTimeouts[LINK_WIRELESS_MAX_PLAYERS];  // (~= LinkCable::msgTimeouts)
    bool recvFlag = false;                       // (~= LinkCable::IRQFlag)
    bool msgFlags[LINK_WIRELESS_MAX_PLAYERS];    // (~= LinkCable::msgFlags)

    bool signalLevelCalled = false;
    bool sendReceiveLatch = false;  // true = send ; false = receive
    bool shouldWaitForServer = false;

    bool didReceiveFirstPacketFromServer = false;
    u32 inflightCount = 0;
    u32 forwardedCount = 0;
    u32 lastPacketId = 0;
    u32 lastPacketIdFromServer = 0;
    u32 lastAckFromServer = 0;
    u32 lastPacketIdFromClients[LINK_WIRELESS_MAX_PLAYERS];
    u32 lastAckFromClients[LINK_WIRELESS_MAX_PLAYERS];
    int lastHeartbeatFromClients[LINK_WIRELESS_MAX_PLAYERS];
    int localHeartbeat = -1;
    volatile bool isResetTimeoutPending = false;
  };

  struct TransferHeader {
    // - This header is appended as the first word of every transfer.
    // - Packets ("messages") are 16-bit.
    // - Packet IDs are 0~63 (server) and 0~15 (clients).
    // - They wrap around, so 31 and 7 are the maximum number of inflight
    //   packets as per the N/2-1 rule.
    // - Messages are sent in order and retransmitted until their ACK. e.g.:
    //   * >> 1, 2, 3
    //   * >> 1, 2, 3, 4, 5
    //   * << ack=3
    //   * >> 4, 5, 6
    //   This wastes bandwidth but reduces latency, since waiting for a
    //   retransmission until not receiving an ACK takes time, and games usually
    //   care more about latency than bandwidth.
    // - The first message can be in the header itself (bits 0~15) when:
    //   * (there *is* something to send) && (it's from a client)
    //   * -> this is indicated with a 1 in `firstPacketId`'s bit 4
    // - Each of the next words in the transfer contain two 16-bit messages.
    //   * Low part first, high part last
    //   * The last word can contain 1 or 2 messages depending on `hasLastMsg`
    // - When `forwarding` is enabled, the server can forward messages from
    //   other clients. If the stream includes forwarded messages, this header
    //   contains `hasPlayerBitMap`=1, and the next halfword is a
    //   `PlayerBitMap`.
    unsigned int _reserved_ : 4;  // unused (or first msg!)
    unsigned int ack4 : 4;        // server: player 4 ACK (or first msg!)
    unsigned int ack3 : 4;        // server: player 3 ACK (or first msg!)
    unsigned int ack2 : 4;        // server: player 2 ACK (or first msg!)
    unsigned int ack1 : 6;        // server: player 1 ACK
                                  // clients: server ACK
    unsigned int hasLastMsg : 1;  // there's a msg in last word's high part
    unsigned int
        hasPlayerBitMap : 1;         // server: next halfword is a PlayerBitMap
    unsigned int firstPacketId : 6;  // next packets are assumed consecutive
                                     // clients only use 4 bits here!
                                     // `hasFirstMsg` is an imaginary flag
                                     // living in `firstPacketId`'s bit 4
    unsigned int playerCount : 2;    // server: playerCount (0 = 2; ...; 3 = 5)
                                     // clients: heartbeat (0~3)
  };

  struct PlayerBitMap {
    // - This halfword is appended after the `TransferHeader` if the stream
    // contains messages from multiple users (when `forwarding` is enabled).
    // - `playerIds` is a bit-packed array of 5 3-bit entries, representing the
    //    owners of the next 5 messages (bits 0~2 = first message's player ID).
    // - `hasMore` indicates whether there are more messages owned by player IDs
    //   greater than 0, which adds another `PlayerBitMap` after the messages.
    // - Each `PlayerBitMap` occupies the space of a regular message, but
    //   doesn't affect the packet ID sequence. By design, they are always
    //   placed in the low part.
    // ---
    // Example: Let's say there's a stream with 12 packets (p1~pC).
    // Before pB, some of the packets are forwarded from other clients.
    // So, the transfer header (THD) takes the first word and sets
    // `hasPlayerBitMap=1`. The low part of the next word is a `PlayerBitMap`
    // (PB) and describes the owners of the following 5 messages (p1~p5). Since
    // there are more forwarded messages (last PB had `hasMore`=1), a new PB is
    // added describing p6~pA. The last two messages (pB~pC) are server
    // messages, so there's no need for a new PB (last PB had `hasMore`=0).
    //   w00    w01    w02    w03    w04    w05    w06    w07
    // |-----||--|--||--|--||--|--||--|--||--|--||--|--||--|--|
    // | THD ||p1|PB||p3|p2||p5|p4||p6|PB||p8|p7||pA|p9||pC|pB|
    // |-----||--|--||--|--||--|--||--|--||--|--||--|--||--|--|
    // ---
    unsigned int playerIds : 15;  // 5 entries, 3 bits per player
    unsigned int hasMore : 1;  // if true, there's another `PlayerBitMap` after
                               // the next 5 messages
  };

  template <typename H>
  union U32Packer {
    H asStruct;
    u32 asInt;
  };

  using CommandResult = LinkRawWireless::CommandResult;

  LinkRawWireless linkRawWireless;
  SessionState sessionState;
  u32 nextAsyncCommandData[LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH];
  u32 nextAsyncCommandDataSize = 0;
  volatile bool isSendingSyncCommand = false;
  volatile Error lastError = Error::NONE;
  volatile bool isEnabled = false;

#ifdef LINK_WIRELESS_ENABLE_NESTED_IRQ
  volatile bool interrupt = false, pendingVBlank = false;

  LINK_INLINE void irqEnd() {
    Link::_REG_IME = 0;
    interrupt = false;
    LINK_BARRIER;
    if (pendingVBlank) {
      _onVBlank();
      pendingVBlank = false;
    }
  }
#endif

  LINK_INLINE void ___onSerial() {
    if (!isEnabled)
      return;

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    profileStart();
#endif

    int status = linkRawWireless._onSerial(false);
    if (status <= -4) {
      return (void)abort(Error::ACKNOWLEDGE_FAILED);
    } else if (status > 0) {
      auto result = linkRawWireless._getAsyncCommandResultRef();
      processAsyncCommand(result);
    }

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    serialTime += profileStop();
    serialIRQs++;
#endif
  }

  LINK_INLINE void ___onTimer() {
    if (!isEnabled)
      return;

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    profileStart();
#endif

    if (!isSessionActive())
      return;

    if (!isAsyncCommandActive())
      checkConnectionsOrTransferData();

#ifdef LINK_WIRELESS_PROFILING_ENABLED
    timerTime += profileStop();
    timerIRQs++;
#endif
  }

  LINK_INLINE void processAsyncCommand(
      const LinkRawWireless::CommandResult* commandResult) {  // (irq only)
    if (!commandResult->success) {
      return (void)abort(
          commandResult->commandId == LinkRawWireless::COMMAND_SEND_DATA
              ? Error::SEND_DATA_FAILED
          : commandResult->commandId == LinkRawWireless::COMMAND_RECEIVE_DATA
              ? Error::RECEIVE_DATA_FAILED
              : Error::COMMAND_FAILED);
    }

    switch (commandResult->commandId) {
      case LinkRawWireless::COMMAND_SIGNAL_LEVEL: {
        // SignalLevel (end)
        u32 levels = commandResult->dataSize > 0 ? commandResult->data[0] : 0;
        u32 players = 1;
        for (u32 i = 1; i < LINK_WIRELESS_MAX_PLAYERS; i++) {
          u32 level = (levels >> ((i - 1) * 8)) & 0xFF;
          sessionState.signalLevel.level[i] = level;
          if (level > 0)
            players++;
        }

        if (players > linkRawWireless.sessionState.playerCount) {
          LINK_BARRIER;
          linkRawWireless.sessionState.playerCount =
              Link::_min(players, config.maxPlayers);
          LINK_BARRIER;
        }

        break;
      }
      case LinkRawWireless::COMMAND_SEND_DATA: {
        // SendData (end)

        if (linkRawWireless.getState() == State::CONNECTED)
          sessionState.shouldWaitForServer = true;
        sessionState.sendReceiveLatch = !sessionState.sendReceiveLatch;

        break;
      }
      case LinkRawWireless::COMMAND_RECEIVE_DATA: {
        // ReceiveData (end)

        sessionState.sendReceiveLatch =
            sessionState.shouldWaitForServer || !sessionState.sendReceiveLatch;

        if (commandResult->dataSize == 0)
          break;

        sessionState.recvFlag = true;
        sessionState.recvTimeout = 0;

        sessionState.shouldWaitForServer = false;

        addIncomingMessagesFromData(commandResult);

        break;
      }
      default: {
      }
    }
  }

  LINK_WIRELESS_TIMER_ISR void checkConnectionsOrTransferData() {  // (irq only)
    if (linkRawWireless.getState() == State::SERVING &&
        !sessionState.signalLevelCalled) {
      // SignalLevel (start)
      if (sendCommandAsync(LinkRawWireless::COMMAND_SIGNAL_LEVEL))
        sessionState.signalLevelCalled = true;
    } else if (linkRawWireless.getState() == State::CONNECTED ||
               isConnected()) {
      bool shouldReceive =
          !sessionState.sendReceiveLatch || sessionState.shouldWaitForServer;

      if (shouldReceive) {
        // ReceiveData (start)
        sendCommandAsync(LinkRawWireless::COMMAND_RECEIVE_DATA);
      } else {
        // SendData (start)
        sendPendingData();
      }
    }
  }

  LINK_WIRELESS_TIMER_ISR void sendPendingData() {  // (irq only)
    copyOutgoingState();

    setDataFromOutgoingMessages();
    if (sendCommandAsync(LinkRawWireless::COMMAND_SEND_DATA, true))
      clearInflightMessagesIfNeeded();
  }

  LINK_WIRELESS_TIMER_ISR void setDataFromOutgoingMessages() {  // (irq only)
    addAsyncData(0, true);  // SendData header (filled later)
    addAsyncData(0);        // Transfer header (filled later)

    bool isServer = linkRawWireless.getState() == State::SERVING;
    u32 maxPacketIds = isServer ? MAX_PACKET_IDS_SERVER : MAX_PACKET_IDS_CLIENT;
    u32 maxInflightPackets =
        isServer ? MAX_INFLIGHT_PACKETS_SERVER : MAX_INFLIGHT_PACKETS_CLIENT;
    u32 maxTransferLength = 1 + getDeviceTransferLength();
    // (+1 for SendData header)

    u32 firstPacketId = NO_ID_ASSIGNED_YET;
    u32 firstMsg = 0;
    u32 msgCount = 0;
    bool highPart = false;
    u32 pendingForwardedCount = sessionState.forwardedCount;
    int currentPlayerBitMapIndex = -1;
    u32 playerBitMapCount = 0;
    sessionState.outgoingMessages.forEach(
        [this, isServer, maxPacketIds, maxInflightPackets, maxTransferLength,
         &firstPacketId, &firstMsg, &msgCount, &highPart,
         &pendingForwardedCount, &currentPlayerBitMapIndex,
         &playerBitMapCount](Message* message) {
          // create packet ID if the packet can be sent
          if (message->packetId == NO_ID_ASSIGNED_YET) {
            if (sessionState.inflightCount < maxInflightPackets) {
              message->packetId = newPacketId(maxPacketIds);
              sessionState.inflightCount++;
            } else {
              return false;
            }
          }

          // get first added packet ID and add first msg if needed
          if (firstPacketId == NO_ID_ASSIGNED_YET) {
            firstPacketId = message->packetId;
            if (!isServer) {
              msgCount++;
              firstMsg = message->data;
              return true;
            }
          }

          // add a new PlayerBitMap if needed
          if (pendingForwardedCount > 0 &&
              (currentPlayerBitMapIndex < 0 ||
               playerBitMapCount == MAX_PLAYER_BITMAP_ENTRIES)) {
            if (playerBitMapCount == MAX_PLAYER_BITMAP_ENTRIES)
              addToAsyncDataShifted(currentPlayerBitMapIndex, 1, BIT_HAS_MORE);

            // `highPart` should always be false here!
            currentPlayerBitMapIndex = nextAsyncCommandDataSize;
            playerBitMapCount = 0;
            addAsyncData(0);
            highPart = true;
          }

          // add to the correct part of the u32
          if (highPart)
            addToLastAsyncDataHalfword(message->data);
          else
            addAsyncData(message->data);
          highPart = !highPart;

          // update player bitmap if needed
          if (currentPlayerBitMapIndex >= 0) {
            addToAsyncDataShifted(currentPlayerBitMapIndex, message->playerId,
                                  PLAYER_ID_BITS * playerBitMapCount);
            playerBitMapCount++;

            if (message->playerId > 0) {
              pendingForwardedCount--;
              if (pendingForwardedCount == 0)
                currentPlayerBitMapIndex = -1;
            }
          }

          msgCount++;

          // only continue if we have available halfwords
          return nextAsyncCommandDataSize < maxTransferLength || highPart;
        });

    // fill Transfer header
    nextAsyncCommandData[1] = buildTransferHeader(isServer, firstPacketId,
                                                  firstMsg, msgCount, highPart);

    // fill SendData header
    u32 bytes = (nextAsyncCommandDataSize - 1) * 4;
    nextAsyncCommandData[0] = linkRawWireless.getSendDataHeaderFor(bytes);
  }

  u32 buildTransferHeader(bool isServer,
                          u32 firstPacketId,
                          u32 firstMsg,
                          u32 msgCount,
                          bool highPart) {  // (irq only)
    TransferHeader transferHeader = {};

    // player count / client heartbeat
    if (isServer) {
      transferHeader.playerCount =
          linkRawWireless.sessionState.playerCount - LINK_WIRELESS_MIN_PLAYERS;
    } else {
      sessionState.localHeartbeat = (sessionState.localHeartbeat + 1) % 4;
      transferHeader.playerCount = sessionState.localHeartbeat;
    }

    // first packet ID, or 0 if there are no messages
    if (msgCount > 0)
      transferHeader.firstPacketId = firstPacketId;

    // the last word has a message in its high part if there are messages and we
    // fully used all words
    transferHeader.hasLastMsg = msgCount > 0 && !highPart;

    // the transfer contains player bitmaps if there are forwarded messages
    transferHeader.hasPlayerBitMap = sessionState.forwardedCount > 0;

    // `ack1` is used by both server and clients
    transferHeader.ack1 = isServer ? sessionState.lastPacketIdFromClients[1]
                                   : sessionState.lastPacketIdFromServer;

    if (isServer) {
      // server use `ack2`, `ack3` and `ack4` for acknowledging P2, P3, P4
      transferHeader.ack2 = sessionState.lastPacketIdFromClients[2];
      transferHeader.ack3 = sessionState.lastPacketIdFromClients[3];
      transferHeader.ack4 = sessionState.lastPacketIdFromClients[4];
    } else if (msgCount > 0) {
      // but clients can use this area for storing the first message (*)
      transferHeader.firstPacketId |= HAS_FIRST_MSG_MASK;
    }

    // interpret the whole thing as u32
    U32Packer<TransferHeader> packer = {};
    packer.asStruct = transferHeader;
    if (!isServer)
      packer.asInt |= firstMsg;  // (*)
    return packer.asInt;
  }

  void clearInflightMessagesIfNeeded() {  // (irq only)
    if (config.retransmission)
      return;

    while (!sessionState.outgoingMessages.isEmpty()) {
      u32 packetId = sessionState.outgoingMessages.peek().packetId;
      if (packetId == NO_ID_ASSIGNED_YET)
        break;

      auto message = sessionState.outgoingMessages.pop();
      if (linkRawWireless.getState() == State::SERVING && message.playerId > 0)
        sessionState.forwardedCount--;
    }

    sessionState.inflightCount = 0;
  }

  LINK_WIRELESS_SERIAL_ISR void addIncomingMessagesFromData(
      const CommandResult* result) {  // (irq only)
    // parse ReceiveData header
    u32 sentBytes[LINK_WIRELESS_MAX_PLAYERS] = {0, 0, 0, 0, 0};
    u32 receiveDataHeader = result->data[0];
    sentBytes[0] = Link::_min(receiveDataHeader & 0b1111111,
                              LinkRawWireless::MAX_TRANSFER_BYTES_SERVER);
    sentBytes[1] = Link::_min((receiveDataHeader >> 8) & 0b11111,
                              LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT);
    sentBytes[2] = Link::_min((receiveDataHeader >> 13) & 0b11111,
                              LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT);
    sentBytes[3] = Link::_min((receiveDataHeader >> 18) & 0b11111,
                              LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT);
    sentBytes[4] = Link::_min((receiveDataHeader >> 23) & 0b11111,
                              LinkRawWireless::MAX_TRANSFER_BYTES_CLIENT);

    bool isServer = linkRawWireless.getState() == State::SERVING;
    u32 cursor = 1;
    u32 startPlayerId = isServer ? 1 : 0;
    u32 endPlayerId = isServer ? linkRawWireless.sessionState.playerCount : 1;

    // server reads from indexes 1~4, clients read from index 0
    for (u32 i = startPlayerId; i < endPlayerId; i++) {
      if (sentBytes[i] % 4 != 0)
        return;  // in our protocol, we always send whole words!

      u32 remainingWords = sentBytes[i] / 4;
      if (remainingWords == 0)
        continue;

      // parse TransferHeader
      U32Packer<TransferHeader> packer;
      packer.asInt = result->data[cursor++];
      remainingWords--;
      TransferHeader header = packer.asStruct;

      // if retransmission is enabled, we update the confirmations based on the
      // ACKs found in the header
      if (config.retransmission) {
        if (isServer) {
          sessionState.lastAckFromClients[i] = header.ack1;
        } else {
          u32 currentPlayerId = linkRawWireless.sessionState.currentPlayerId;
          sessionState.lastAckFromServer = currentPlayerId == 1   ? header.ack1
                                           : currentPlayerId == 2 ? header.ack2
                                           : currentPlayerId == 3 ? header.ack3
                                                                  : header.ack4;
        }
      }

      // clients update their player count based on the transfer header
      if (!isServer) {
        LINK_BARRIER;
        linkRawWireless.sessionState.playerCount =
            LINK_WIRELESS_MIN_PLAYERS + header.playerCount;
        LINK_BARRIER;
      }

      // clients can send their first message in the header itself
      u32 currentPacketId = header.firstPacketId;
      bool hasFirstMsg =
          isServer && (currentPacketId & HAS_FIRST_MSG_MASK) != 0;
      if (hasFirstMsg) {
        currentPacketId &= ~HAS_FIRST_MSG_MASK;
        u32 playerBitMap = 0;
        int playerBitMapCount = -1;
        processMessage(i, Link::lsB32(packer.asInt), currentPacketId,
                       playerBitMap, playerBitMapCount);
      }

      // process the remaining words as message pairs
      u32 playerBitMap = 0;
      int playerBitMapCount =
          !isServer && header.hasPlayerBitMap ? MAX_PLAYER_BITMAP_ENTRIES : -1;
      while (remainingWords > 0) {
        bool hasHighPart = remainingWords > 1 || header.hasLastMsg;

        u32 word = result->data[cursor];
        u32 lowPart = Link::lsB32(word);
        if (playerBitMapCount >= MAX_PLAYER_BITMAP_ENTRIES) {
          playerBitMap = lowPart;
          playerBitMapCount = 0;
        } else
          processMessage(i, lowPart, currentPacketId, playerBitMap,
                         playerBitMapCount);

        if (hasHighPart) {
          u32 highPart = Link::msB32(word);
          processMessage(i, highPart, currentPacketId, playerBitMap,
                         playerBitMapCount);
        }

        cursor++;
        remainingWords--;
      }

      bool shouldResetTimeouts = true;
      if (isServer) {
        // reset timeouts, only if the heartbeat from the clients changed (*)
        int heartbeat = header.playerCount;
        shouldResetTimeouts =
            heartbeat != sessionState.lastHeartbeatFromClients[i];
        sessionState.lastHeartbeatFromClients[i] = heartbeat;
        // (*) sometimes, when a client is disconnected, the Wireless Adapter
        // keeps repeating old data in its slot! we use this heartbeat to verify
        // that the client is still generating packets actively!
      }

      if (shouldResetTimeouts) {
        sessionState.msgTimeouts[0] = 0;
        sessionState.msgTimeouts[i] = 0;
        sessionState.msgFlags[0] = true;
        sessionState.msgFlags[i] = true;
      }
    }

    // remove confirmed messages based on the updated ACKs
    if (config.retransmission) {
      if (isServer)
        removeConfirmedMessagesFromClients();
      else
        removeConfirmedMessagesFromServer();
    }

    // copy data from the interrupt world to the main world
    copyIncomingState();
  }

  LINK_WIRELESS_ISR_FUNC(
      processMessage,
      (u32 playerId,
       u32 data,
       u32& currentPacketId,
       u32& playerBitMap,
       int& playerBitMapCount),
      (playerId, data, currentPacketId, playerBitMap, playerBitMapCount),
      {
        // (irq only)
        // store the packet ID and increment (msgs are consecutive inside
        // transfers)
        u32 packetId = currentPacketId;
        currentPacketId =
            (currentPacketId + 1) %
            (playerId == 0 ? MAX_PACKET_IDS_SERVER : MAX_PACKET_IDS_CLIENT);

        // get msg player ID based on player bitmap
        u32 msgPlayerId = playerId;
        if (playerBitMapCount >= 0) {
          msgPlayerId = (playerBitMap >> PLAYER_ID_BITS * playerBitMapCount) &
                        PLAYER_ID_MASK;
          playerBitMapCount++;

          if (playerBitMapCount >= MAX_PLAYER_BITMAP_ENTRIES &&
              !((playerBitMap >> BIT_HAS_MORE) & 1))
            playerBitMapCount = -1;
        }

        if (playerId == 0 && !sessionState.didReceiveFirstPacketFromServer) {
          // the first time clients receive something from the server,
          // they shouldn't have any expectations (since they can join at any
          // time)
          sessionState.lastPacketIdFromServer = packetId;
          sessionState.didReceiveFirstPacketFromServer = true;
        } else {
          // if retransmission is enabled, the packet ID needs to be expected
          if (config.retransmission) {
            u32 expectedPacketId =
                playerId > 0
                    ? (sessionState.lastPacketIdFromClients[playerId] + 1) %
                          MAX_PACKET_IDS_CLIENT
                    : (sessionState.lastPacketIdFromServer + 1) %
                          MAX_PACKET_IDS_SERVER;

            if (packetId != expectedPacketId)
              return;

            if (playerId > 0)
              sessionState.lastPacketIdFromClients[playerId] = expectedPacketId;
            else
              sessionState.lastPacketIdFromServer = expectedPacketId;
          }
        }

        // ignore messages from myself
        if (msgPlayerId == linkRawWireless.sessionState.currentPlayerId)
          return;

        // add new message
        Message message;
        message.playerId = msgPlayerId;
        message.data = data;
        message.packetId = packetId;
        sessionState.newIncomingMessages.push(message);

        // forward to other clients if needed
        if (playerId > 0 && config.forwarding &&
            linkRawWireless.sessionState.playerCount > 2)
          forwardMessage(message);
      })

  LINK_WIRELESS_SERIAL_ISR void forwardMessage(
      Message& message) {  // (irq only)
    Message forwardedMessage;
    forwardedMessage.data = message.data;
    forwardedMessage.playerId = message.playerId;
    if (!sessionState.outgoingMessages.isFull()) {
      sessionState.outgoingMessages.push(forwardedMessage);
      sessionState.forwardedCount++;
    } else
      sessionState.outgoingMessages.overflow = true;
  }

  LINK_WIRELESS_SERIAL_ISR void
  removeConfirmedMessagesFromServer() {  // (irq only)
    removeConfirmedMessages(sessionState.lastAckFromServer,
                            MAX_PACKET_IDS_CLIENT, MAX_INFLIGHT_PACKETS_CLIENT);
  }

  LINK_WIRELESS_SERIAL_ISR void
  removeConfirmedMessagesFromClients() {  // (irq only)
    u32 ringMinAck = 0xFFFFFFFF;
    for (u32 i = 1; i < linkRawWireless.sessionState.playerCount; i++) {
      u32 ack = sessionState.lastAckFromClients[i];

      // ignore clients that didn't confirm anything yet
      if (ack == NO_ACK_RECEIVED_YET)
        continue;

      if (ringMinAck == 0xFFFFFFFF) {
        // on first time, we set `ringMinAck`
        ringMinAck = ack;
      } else {
        // we compare `ringMinAck` vs `ack` in circular space
        // (0..MAX_PACKET_IDS_CLIENT-1):
        //   -> how many steps it is from `ringMinAck` down to `ack`?
        u32 dist = (ringMinAck - ack) & (MAX_PACKET_IDS_CLIENT - 1);

        // if dist >= MAX_INFLIGHT_PACKETS_CLIENT => `ack` is "behind"
        // `ringMinAck`, so we replace it!
        if (dist >= MAX_INFLIGHT_PACKETS_CLIENT)
          ringMinAck = ack;
      }
    }

    // if we found a valid minimum ack across all clients, we remove!
    if (ringMinAck != 0xFFFFFFFF)
      removeConfirmedMessages(ringMinAck, MAX_PACKET_IDS_SERVER,
                              MAX_INFLIGHT_PACKETS_SERVER);
  }

  LINK_WIRELESS_SERIAL_ISR void removeConfirmedMessages(
      u32 ack,
      const u32 maxPacketIds,
      const u32 maxInflightPackets) {  // (irq only)
    while (!sessionState.outgoingMessages.isEmpty()) {
      u32 packetId = sessionState.outgoingMessages.peek().packetId;

      // if the current message is not inflight, we've entered the section of
      // 'new' messages (with no ID assigned), so we quit!
      if (packetId == NO_ID_ASSIGNED_YET)
        break;

      // we release the packet if it was confirmed (aka inside the send window)
      // example with maxPacketIds=16, maxInflightPackets=7, ack=4:
      //   => we would be releasing packets 4,3,2,1,15,14,13
      if (((ack - packetId) & (maxPacketIds - 1)) <= maxInflightPackets) {
        auto message = sessionState.outgoingMessages.pop();
        sessionState.inflightCount--;
        if (maxPacketIds == MAX_PACKET_IDS_SERVER && message.playerId > 0)
          sessionState.forwardedCount--;
      } else
        break;
    }
  }

  LINK_WIRELESS_TIMER_ISR u32 getDeviceTransferLength() {  // (irq only)
    return linkRawWireless.getState() == State::SERVING
               ? LINK_WIRELESS_MAX_SERVER_TRANSFER_LENGTH
               : LINK_WIRELESS_MAX_CLIENT_TRANSFER_LENGTH;
  }

  LINK_WIRELESS_TIMER_ISR void copyOutgoingState() {  // (irq only)
    if (sessionState.newOutgoingMessages.isWriting())
      return;

    while (!sessionState.newOutgoingMessages.isEmpty() &&
           !sessionState.outgoingMessages.isFull()) {
      auto message = sessionState.newOutgoingMessages.pop();
      sessionState.outgoingMessages.push(message);
    }
  }

  LINK_WIRELESS_SERIAL_ISR void copyIncomingState() {  // (irq only)
    if (sessionState.incomingMessages.isReading())
      return;

    while (!sessionState.newIncomingMessages.isEmpty() &&
           !sessionState.incomingMessages.isFull()) {
      auto message = sessionState.newIncomingMessages.pop();
      sessionState.incomingMessages.push(message);
    }
  }

  bool checkRemoteTimeouts() {  // (irq only)
    bool isServer = linkRawWireless.getState() == State::SERVING;
    u32 startPlayerId = isServer ? 1 : 0;
    u32 endPlayerId = isServer ? linkRawWireless.sessionState.playerCount : 1;

    for (u32 i = startPlayerId; i < endPlayerId; i++) {
      if (!sessionState.msgFlags[i]) {
        sessionState.msgTimeouts[i]++;
        if (sessionState.msgTimeouts[i] > config.timeout)
          return false;
      }
      sessionState.msgFlags[i] = false;
    }

    return true;
  }

  LINK_WIRELESS_TIMER_ISR u32 newPacketId(u32 maxPacketIds) {  // (irq only)
    return (sessionState.lastPacketId =
                (sessionState.lastPacketId + 1) % maxPacketIds);
  }

  LINK_WIRELESS_TIMER_ISR void addToLastAsyncDataHalfword(
      u16 value) {  // (irq only)
    addToAsyncDataShifted(nextAsyncCommandDataSize - 1, value, 16);
  }

  LINK_WIRELESS_TIMER_ISR void addToAsyncDataShifted(u32 index,
                                                     u16 value,
                                                     u32 shift) {  // (irq only)
    nextAsyncCommandData[index] |= value << shift;
  }

  LINK_WIRELESS_TIMER_ISR void addAsyncData(u32 value,
                                            bool start = false) {  // (irq only)
    if (start)
      nextAsyncCommandDataSize = 0;
    nextAsyncCommandData[nextAsyncCommandDataSize] = value;
    nextAsyncCommandDataSize++;
  }

  bool sendCommandAsync(u8 type,
                        bool withData = false) {  // (irq only)
    if (isSendingSyncCommand)
      return false;

    u32 size = withData ? nextAsyncCommandDataSize : 0;
    return linkRawWireless.sendCommandAsync(type, nextAsyncCommandData, size,
                                            false, true);
  }

  bool isAsyncCommandActive() {
    return linkRawWireless.getAsyncState() ==
           LinkRawWireless::AsyncState::WORKING;
  }

  bool badRequest(Error error) {
    isSendingSyncCommand = false;
    lastError = error;
    return false;
  }

  bool abort(Error error) {
    reset();
    lastError = error;
    return false;
  }

  bool reset() {
    bool wasEnabled = isEnabled;

    LINK_BARRIER;
    isEnabled = false;
    LINK_BARRIER;

    resetState();
    stop();
    bool success = start();

    LINK_BARRIER;
    isEnabled = wasEnabled;
    LINK_BARRIER;

    return success;
  }

  void resetState() {
    LINK_BARRIER;
    linkRawWireless._resetState();

    sessionState.recvFlag = false;
    sessionState.recvTimeout = 0;
    sessionState.signalLevelCalled = false;
    sessionState.sendReceiveLatch = false;
    sessionState.shouldWaitForServer = false;
    sessionState.didReceiveFirstPacketFromServer = false;
    sessionState.inflightCount = 0;
    sessionState.forwardedCount = 0;
    sessionState.lastPacketId = 0;
    sessionState.lastPacketIdFromServer = 0;
    sessionState.lastAckFromServer = 0;
    sessionState.localHeartbeat = -1;
    sessionState.isResetTimeoutPending = false;
    for (u32 i = 0; i < LINK_WIRELESS_MAX_PLAYERS; i++) {
      sessionState.msgTimeouts[i] = 0;
      sessionState.msgFlags[i] = false;
      sessionState.lastPacketIdFromClients[i] = 0;
      sessionState.lastAckFromClients[i] = NO_ACK_RECEIVED_YET;
      sessionState.lastHeartbeatFromClients[i] = -1;
    }
    nextAsyncCommandDataSize = 0;

    sessionState.incomingMessages.syncClear();
    sessionState.outgoingMessages.clear();

    sessionState.newIncomingMessages.clear();
    sessionState.newOutgoingMessages.syncClear();

    sessionState.newIncomingMessages.overflow = false;
    sessionState.signalLevel = SignalLevel{};

    isSendingSyncCommand = false;
    LINK_BARRIER;
  }

  void stop() {
    stopTimer();
    linkRawWireless.deactivate();
  }

  bool start() {
    startTimer();

    if (!linkRawWireless.activate(false))
      return false;

    if (!setup())
      return false;

    return true;
  }

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

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

  bool setup(u8 maxPlayers = LINK_WIRELESS_MAX_PLAYERS) {
    return linkRawWireless.setup(maxPlayers);
  }

  template <typename F>
  void waitVBlanks(u32 vBlanks, F onVBlank) {
    u32 count = 0;
    u32 vCount = Link::_REG_VCOUNT;

    while (count < vBlanks) {
      if (Link::_REG_VCOUNT != vCount) {
        vCount = Link::_REG_VCOUNT;

        if (vCount == 160) {
          onVBlank();
          count++;
        }
      }
    };
  }

#ifdef LINK_WIRELESS_PROFILING_ENABLED
  void profileStart() {
    Link::_REG_TM1CNT_L = 0;
    Link::_REG_TM2CNT_L = 0;

    Link::_REG_TM1CNT_H = 0;
    Link::_REG_TM2CNT_H = 0;

    Link::_REG_TM2CNT_H = Link::_TM_ENABLE | Link::_TM_CASCADE;
    Link::_REG_TM1CNT_H = Link::_TM_ENABLE | Link::_TM_FREQ_1;
  }

  u32 profileStop() {
    Link::_REG_TM1CNT_H = 0;
    Link::_REG_TM2CNT_H = 0;

    return (Link::_REG_TM1CNT_L | (Link::_REG_TM2CNT_L << 16));
  }
#endif
};

extern LinkWireless* linkWireless;

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

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

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

/**
 * NOTES:
 * When using `LINK_WIRELESS_ENABLE_NESTED_IRQ`:
 *   - Any user ISR can interrupt the library ISRs.
 *     * The SERIAL ISR only enables nested interrupts after completing the
 *       acknowledge with the Wireless Adapter.
 *   - SERIAL ISR can interrupt TIMER ISR.
 *     -> This doesn't cause data races since TIMER ISR only works when
 *        there is no active async task.
 *     -> When TIMER ISR starts an async task (`transferAsync(...)`),
 *        nested interrupts are disabled (`REG_IME = 0`) and SERIAL cannot
 *        interrupt anymore.
 *   - TIMER interrupts are skipped if SERIAL ISR is running.
 *   - VBLANK interrupts are postponed if SERIAL or TIMER ISRs are running.
 *   - Nobody can interrupt VBLANK ISR.
 */

#endif  // LINK_WIRELESS_H
