#ifndef LINK_MOBILE_H
#define LINK_MOBILE_H

// --------------------------------------------------------------------------
// A high level driver for the Mobile Adapter GB.
// Check out the REON project -> https://github.com/REONTeam
// --------------------------------------------------------------------------
// Usage:
// - 1) Include this header in your main.cpp file and add:
//       LinkMobile* linkMobile = new LinkMobile();
// - 2) Add the required interrupt service routines: (*)
//       interrupt_init();
//       interrupt_add(INTR_VBLANK, LINK_MOBILE_ISR_VBLANK);
//       interrupt_add(INTR_SERIAL, LINK_MOBILE_ISR_SERIAL);
//       interrupt_add(INTR_TIMER3, LINK_MOBILE_ISR_TIMER);
// - 3) Initialize the library with:
//       linkMobile->activate();
//       // (do something until `linkMobile->isSessionActive()` returns `true`)
// - 4) Call someone:
//       linkMobile->call("127000000001");
//       // (do something until `linkMobile->isConnectedP2P()` returns `true`)
// - 5) Send/receive data:
//       LinkMobile::DataTransfer dataTransfer = { .size = 5 };
//       for (u32 i = 0; i < 5; i++)
//         dataTransfer.data[i] = ((u8*)"hello")[i];
//       linkMobile->transfer(dataTransfer, &dataTransfer);
//       // (do something until `dataTransfer.completed` is `true`)
//       // (use `dataTransfer` as the received data)
// - 6) Hang up:
//       linkMobile->hangUp();
// - 7) Connect to the internet:
//       linkMobile->callISP("REON password");
//       // (do something until `linkMobile->isConnectedPPP()` returns `true`)
// - 8) Run DNS queries:
//       LinkMobile::DNSQuery dnsQuery;
//       linkMobile->dnsQuery("something.com", &dnsQuery);
//       // (do something until `dnsQuery.completed` is `true`)
//       // (use `dnsQuery.success` and `dnsQuery.ipv4`)
// - 9) Open connections:
//       auto type = LinkMobile::ConnectionType::TCP;
//       LinkMobile::OpenConn openConn;
//       linkMobile->openConnection(dnsQuery.ipv4, connType, &openConn);
//       // (do something until `openConn.completed` is `true`)
//       // (use `openConn.connectionId` as last argument of `transfer(...)`)
// - 10) Close connections:
//       LinkMobile::CloseConn closeConn;
//       linkMobile->closeConnection(openConn.connectionId, type, &closeConn);
//       // (do something until `closeConn.completed` is `true`)
// - 11) Synchronously wait for an action to be completed:
//       linkMobile->waitFor(&dnsQuery);
// - 12) Turn off the adapter:
//       linkMobile->shutdown();
// --------------------------------------------------------------------------
// (*) 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 "LinkGPIO.hpp"
#include "LinkSPI.hpp"

#ifndef LINK_MOBILE_QUEUE_SIZE
/**
 * @brief Request queue size (how many commands can be queued at the same time).
 * The default value is `10`, which seems fine for most games.
 * \warning This affects how much memory is allocated. With the default value,
 * it's around 3 KB.
 */
#define LINK_MOBILE_QUEUE_SIZE 10
#endif

LINK_VERSION_TAG LINK_MOBILE_VERSION = "vLinkMobile/v8.0.0";

#define LINK_MOBILE_MAX_USER_TRANSFER_LENGTH 254
#define LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH 255
#define LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH 32
#define LINK_MOBILE_MAX_LOGIN_ID_LENGTH 32
#define LINK_MOBILE_MAX_PASSWORD_LENGTH 32
#define LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH 253
#define LINK_MOBILE_COMMAND_TRANSFER_BUFFER \
  (LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH + 4)
#define LINK_MOBILE_DEFAULT_TIMEOUT (60 * 10)
#define LINK_MOBILE_DEFAULT_TIMER_ID 3

#if LINK_ENABLE_DEBUG_LOGS != 0
#define _LMLOG_(...) Link::log(__VA_ARGS__)
#else
#define _LMLOG_(...)
#endif

/**
 * @brief A high level driver for the Mobile Adapter GB.
 */
class LinkMobile {
 private:
  using u32 = Link::u32;
  using u16 = Link::u16;
  using u8 = Link::u8;

  static constexpr auto BASE_FREQUENCY = Link::_TM_FREQ_1024;
  static constexpr int INIT_WAIT_FRAMES = 7;
  static constexpr int INIT_TIMEOUT_FRAMES = 60 * 3;
  static constexpr int PING_FREQUENCY_FRAMES = 60;
  static constexpr int ADAPTER_WAITING = 0xD2;
  static constexpr u32 ADAPTER_WAITING_32BIT = 0xD2D2D2D2;
  static constexpr int GBA_WAITING = 0x4B;
  static constexpr u32 GBA_WAITING_32BIT = 0x4B4B4B4B;
  static constexpr int OR_VALUE = 0x80;
  static constexpr int COMMAND_MAGIC_VALUE1 = 0x99;
  static constexpr int COMMAND_MAGIC_VALUE2 = 0x66;
  static constexpr int DEVICE_GBA = 0x1;
  static constexpr int DEVICE_ADAPTER_BLUE = 0x8;
  static constexpr int DEVICE_ADAPTER_YELLOW = 0x9;
  static constexpr int DEVICE_ADAPTER_GREEN = 0xA;
  static constexpr int DEVICE_ADAPTER_RED = 0xB;
  static constexpr int ACK_SENDER = 0;
  static constexpr int CONFIGURATION_DATA_SIZE = 192;
  static constexpr int CONFIGURATION_DATA_CHUNK = CONFIGURATION_DATA_SIZE / 2;
  static constexpr const char* FALLBACK_ISP_NUMBER = "#9677";
  static constexpr int COMMAND_BEGIN_SESSION = 0x10;
  static constexpr int COMMAND_END_SESSION = 0x11;
  static constexpr int COMMAND_DIAL_TELEPHONE = 0x12;
  static constexpr int COMMAND_HANG_UP_TELEPHONE = 0x13;
  static constexpr int COMMAND_WAIT_FOR_TELEPHONE_CALL = 0x14;
  static constexpr int COMMAND_TRANSFER_DATA = 0x15;
  static constexpr int COMMAND_RESET = 0x16;
  static constexpr int COMMAND_TELEPHONE_STATUS = 0x17;
  static constexpr int COMMAND_SIO32 = 0x18;
  static constexpr int COMMAND_READ_CONFIGURATION_DATA = 0x19;
  static constexpr int COMMAND_ISP_LOGIN = 0x21;
  static constexpr int COMMAND_ISP_LOGOUT = 0x22;
  static constexpr int COMMAND_OPEN_TCP_CONNECTION = 0x23;
  static constexpr int COMMAND_CLOSE_TCP_CONNECTION = 0x24;
  static constexpr int COMMAND_OPEN_UDP_CONNECTION = 0x25;
  static constexpr int COMMAND_CLOSE_UDP_CONNECTION = 0x26;
  static constexpr int COMMAND_DNS_QUERY = 0x28;
  static constexpr int COMMAND_CONNECTION_CLOSED = 0x1F;
  static constexpr int COMMAND_ERROR_STATUS = 0x6E | OR_VALUE;
  static constexpr u8 WAIT_TICKS[] = {4, 8};
  static constexpr int LOGIN_PARTS_SIZE = 8;
  static constexpr u8 LOGIN_PARTS[] = {0x4E, 0x49, 0x4E, 0x54,
                                       0x45, 0x4E, 0x44, 0x4F};
  static constexpr int SUPPORTED_DEVICES_SIZE = 4;
  static constexpr u8 SUPPORTED_DEVICES[] = {
      DEVICE_ADAPTER_BLUE, DEVICE_ADAPTER_YELLOW, DEVICE_ADAPTER_GREEN,
      DEVICE_ADAPTER_RED};
  static constexpr u8 DIAL_PHONE_FIRST_BYTE[] = {0, 2, 1, 1};

 public:
  enum class State {
    NEEDS_RESET = 0,
    PINGING = 1,
    WAITING_TO_START = 2,
    STARTING_SESSION = 3,
    ACTIVATING_SIO32 = 4,
    WAITING_32BIT_SWITCH = 5,
    READING_CONFIGURATION = 6,
    SESSION_ACTIVE = 7,
    CALL_REQUESTED = 8,
    CALLING = 9,
    CALL_ESTABLISHED = 10,
    ISP_CALL_REQUESTED = 11,
    ISP_CALLING = 12,
    PPP_LOGIN = 13,
    PPP_ACTIVE = 14,
    SHUTDOWN_REQUESTED = 15,
    ENDING_SESSION = 16,
    WAITING_8BIT_SWITCH = 17,
    SHUTDOWN = 18
  };

  enum class Role { NO_P2P_CONNECTION, CALLER, RECEIVER };

  enum class ConnectionType { TCP, UDP };

  struct ConfigurationData {
    char magic[2];
    u8 registrationState;
    u8 _unused1_;
    u8 primaryDNS[4];
    u8 secondaryDNS[4];
    char loginId[10];
    u8 _unused2_[22];
    char email[24];
    u8 _unused3_[6];
    char smtpServer[20];
    char popServer[19];
    u8 _unused4_[5];
    u8 configurationSlot1[24];
    u8 configurationSlot2[24];
    u8 configurationSlot3[24];
    u8 checksumHigh;
    u8 checksumLow;

    char _ispNumber1[16 + 1];  // (parsed from `configurationSlot1`)
  } LINK_PACKED;

  struct AsyncRequest {
    volatile bool completed = false;
    bool success = false;

    bool fail() {
      success = false;
      completed = true;
      return false;
    }
  };

  struct DNSQuery : public AsyncRequest {
    u8 ipv4[4] = {};
  };

  struct OpenConn : public AsyncRequest {
    u8 connectionId = 0;
  };

  struct CloseConn : public AsyncRequest {};

  struct DataTransfer : public AsyncRequest {
    u8 data[LINK_MOBILE_MAX_USER_TRANSFER_LENGTH] = {};
    u8 size = 0;
  };

  enum class CommandResult {
    PENDING,
    SUCCESS,
    INVALID_DEVICE_ID,
    INVALID_COMMAND_ACK,
    INVALID_MAGIC_BYTES,
    WEIRD_DATA_SIZE,
    WRONG_CHECKSUM,
    ERROR_CODE,
    WEIRD_ERROR_CODE
  };

  struct Error {
    enum class Type {
      NONE,
      ADAPTER_NOT_CONNECTED,
      PPP_LOGIN_FAILED,
      COMMAND_FAILED,
      WEIRD_RESPONSE,
      TIMEOUT,
      WTF
    };

    Error::Type type = Error::Type::NONE;
    State state = State::NEEDS_RESET;
    u8 cmdId = 0;
    CommandResult cmdResult = CommandResult::PENDING;
    u8 cmdErrorCode = 0;
    bool cmdIsSending = false;
    int reqType = -1;
  };

  /**
   * @brief Constructs a new LinkMobile object.
   * @param timeout Number of *frames* without completing a request to reset a
   * connection. Defaults to 600 (10 seconds).
   * @param timerId `(0~3)` GBA Timer to use for waiting.
   */
  explicit LinkMobile(u32 timeout = LINK_MOBILE_DEFAULT_TIMEOUT,
                      u8 timerId = LINK_MOBILE_DEFAULT_TIMER_ID) {
    config.timeout = timeout;
    config.timerId = timerId;
  }

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

  /**
   * @brief Activates the library. After some time, if an adapter is connected,
   * the state will be changed to `SESSION_ACTIVE`. If not, the state will be
   * `NEEDS_RESET`, and you can retrieve the error with `getError()`.
   */
  void activate() {
    LINK_READ_TAG(LINK_MOBILE_VERSION);
    static_assert(LINK_MOBILE_QUEUE_SIZE >= 1);

    error = {};

    LINK_BARRIER;
    isEnabled = false;
    LINK_BARRIER;

    resetState();
    stop();

    LINK_BARRIER;
    isEnabled = true;
    LINK_BARRIER;

    start();
  }

  /**
   * @brief Deactivates the library, resetting the serial mode to GPIO.
   * \warning Calling `shutdown()` first is recommended, but the adapter will
   * put itself in sleep mode after 3 seconds anyway.
   */
  void deactivate() {
    error = {};
    isEnabled = false;
    resetState();
    stop();
  }

  /**
   * @brief Gracefully shuts down the adapter, closing all connections.
   * After some time, the state will be changed to `SHUTDOWN`, and only then
   * it's safe to call `deactivate()`.
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active session or available request slots.
   */
  bool shutdown() {
    if (!canShutdown() || userRequests.isFull())
      return false;

    pushRequest(UserRequest{.type = UserRequest::Type::SHUTDOWN});
    return true;
  }

  /**
   * @brief Initiates a P2P connection with a `phoneNumber`. After some time,
   * the state will be `CALL_ESTABLISHED` (or `ACTIVE_SESSION` if the
   * connection fails or ends).
   * @param phoneNumber The phone number to call. In REON/libmobile this can be
   * a number assigned by the relay server, or a 12-digit IPv4 address (for
   * example, "127000000001" would be 127.0.0.1).
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active session or available request slots.
   */
  bool call(const char* phoneNumber) {
    if (state != State::SESSION_ACTIVE || userRequests.isFull())
      return false;

    auto request = UserRequest{.type = UserRequest::Type::CALL};
    copyString(request.phoneNumber, phoneNumber,
               LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH);
    pushRequest(request);
    return true;
  }

  /**
   * @brief Calls the ISP number registered in the adapter configuration, or a
   * default number if the adapter hasn't been configured. Then, performs a
   * login operation using the provided REON `password` and `loginId`. After
   * some time, the state will be `PPP_ACTIVE`. If `loginId` is empty and the
   * adapter has been configured, it will use the one stored in the
   * configuration.
   * @param password The password, as a null-terminated string (max `32`
   * characters).
   * @param loginId The login ID, as a null-terminated string (max `32`
   * characters). It can be empty if it's already stored in the configuration.
   * \warning Non-blocking. Returns `true` immediately, or `false`
   * if there's no active session, no available request slots, or no login ID.
   */
  bool callISP(const char* password, const char* loginId = "") {
    if (state != State::SESSION_ACTIVE || userRequests.isFull())
      return false;

    auto request = UserRequest{.type = UserRequest::Type::PPP_LOGIN};
    copyString(request.password, password, LINK_MOBILE_MAX_PASSWORD_LENGTH);

    if (Link::strlen(loginId) > 0)
      copyString(request.loginId, loginId, LINK_MOBILE_MAX_LOGIN_ID_LENGTH);
    else if (adapterConfiguration.isValid())
      copyString(request.loginId, adapterConfiguration.fields._ispNumber1,
                 LINK_MOBILE_MAX_LOGIN_ID_LENGTH);
    else
      return false;

    pushRequest(request);
    return true;
  }

  /**
   * @brief Looks up the IPv4 address for a domain name.
   * @param domain A null-terminated string for the domain name (max `253`
   * characters). It also accepts a ASCII IPv4 address, converting it into a
   * 4-byte address instead of querying the DNS server.
   * @param result A pointer to a `LinkMobile::DNSQuery` struct that
   * will be filled with the result. When the request is completed, the
   * `completed` field will be `true`. If an IP address was found, the `success`
   * field will be `true` and the `ipv4` field can be read as a 4-byte address.
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active PPP session or available request slots.
   */
  bool dnsQuery(const char* domainName, DNSQuery* result) {
    if (state != State::PPP_ACTIVE || userRequests.isFull())
      return result->fail();

    result->completed = false;
    result->success = false;
    u32 size = Link::strlen(domainName);
    if (size > LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH)
      size = LINK_MOBILE_MAX_DOMAIN_NAME_LENGTH;

    auto request = UserRequest{.type = UserRequest::Type::DNS_QUERY,
                               .dns = result,
                               .send = {.data = {}},
                               .commandSent = false};
    for (u32 i = 0; i < size; i++)
      request.send.data[i] = domainName[i];
    request.send.size = size;

    pushRequest(request);
    return true;
  }

  /**
   * @brief Opens a TCP/UDP (`type`) connection at the given `ip` (4-byte
   * address) on the given `port`.
   * @param ip The 4-byte address.
   * @param port The port.
   * @param type One of the enum values from `LinkMobile::ConnectionType`.
   * @param result A pointer to a `LinkMobile::OpenConn` struct that
   * will be filled with the result. When the request is completed, the
   * `completed` field will be `true`. If the connection was successful, the
   * `success` field will be `true` and the `connectionId` field can be used
   * when calling the `transfer(...)` method. If not, you can assume that the
   * connection was closed.
   * \warning Only `2` connections can be opened at the same time.
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active PPP session, no available request slots.
   */
  bool openConnection(const u8* ip,
                      u16 port,
                      ConnectionType type,
                      OpenConn* result) {
    if (state != State::PPP_ACTIVE || userRequests.isFull())
      return result->fail();

    result->completed = false;
    result->success = false;

    auto request = UserRequest{.type = UserRequest::Type::OPEN_CONNECTION,
                               .open = result,
                               .connectionType = type,
                               .commandSent = false};
    for (u32 i = 0; i < 4; i++)
      request.ip[i] = ip[i];
    request.port = port;

    pushRequest(request);
    return true;
  }

  /**
   * @brief Closes an active TCP/UDP (`type`) connection.
   * @param connectionId The ID of the connection.
   * @param type One of the enum values from `LinkMobile::ConnectionType`.
   * @param result A pointer to a `LinkMobile::CloseConn` struct that
   * will be filled with the result. When the request is completed, the
   * `completed` field will be `true`. If the connection was closed correctly,
   * the `success` field will be `true`.
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active PPP session, no available request slots.
   */
  bool closeConnection(u8 connectionId,
                       ConnectionType type,
                       CloseConn* result) {
    if (state != State::PPP_ACTIVE || userRequests.isFull())
      return result->fail();

    result->completed = false;
    result->success = false;

    auto request = UserRequest{.type = UserRequest::Type::CLOSE_CONNECTION,
                               .close = result,
                               .connectionType = type,
                               .commandSent = false};
    request.connectionId = connectionId;

    pushRequest(request);
    return true;
  }

  /**
   * @brief Requests a data transfer and responds the received data. The
   * transfer can be done with the other node in a P2P connection, or with any
   * open TCP/UDP connection if a PPP session is active. In the case of a
   * TCP/UDP connection, the `connectionId` must be provided.
   * @param dataToSend The data to send, up to 254 bytes.
   * @param result A pointer to a `LinkMobile::DataTransfer` struct that
   * will be filled with the received data. It can also point to `dataToSend` to
   * reuse the struct. When the transfer is completed, the `completed` field
   * will be `true`. If the transfer was successful, the `success` field will be
   * `true`.
   * @param connectionId The ID of the connection (or `0xFF` for P2P).
   * \warning Non-blocking. Returns `true` immediately, or `false` if
   * there's no active call or available request slots.
   */
  bool transfer(DataTransfer dataToSend,
                DataTransfer* result,
                u8 connectionId = 0xFF) {
    if ((state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) ||
        userRequests.isFull())
      return result->fail();

    result->completed = false;
    result->success = false;

    auto request = UserRequest{.type = UserRequest::Type::TRANSFER,
                               .connectionId = connectionId,
                               .send = {.data = {}, .size = dataToSend.size},
                               .receive = result,
                               .commandSent = false};
    for (u32 i = 0; i < dataToSend.size; i++)
      request.send.data[i] = dataToSend.data[i];
    pushRequest(request);
    return true;
  }

  /**
   * @brief Waits for `asyncRequest` to be completed. Returns `true` if the
   * request was completed && successful, and the adapter session is still
   * alive. Otherwise, it returns `false`.
   * @param asyncRequest A pointer to a `LinkMobile::DNSQuery`,
   * `LinkMobile::OpenConn`, `LinkMobile::CloseConn`, or
   * `LinkMobile::DataTransfer`.
   */
  bool waitFor(AsyncRequest* asyncRequest) {
    while (isSessionActive() && !asyncRequest->completed)
      Link::_IntrWait(1, Link::_IRQ_SERIAL | Link::_IRQ_VBLANK);

    return isSessionActive() && asyncRequest->completed &&
           asyncRequest->success;
  }

  /**
   * @brief Hangs up the current P2P or PPP call. Closes all connections.
   * \warning Non-blocking. Returns `true` immediately, or `false` if there's no
   * active call or available request slots.
   */
  bool hangUp() {
    if ((state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) ||
        userRequests.isFull())
      return false;

    pushRequest(UserRequest{.type = UserRequest::Type::HANG_UP});
    return true;
  }

  /**
   * @brief Retrieves the adapter configuration.
   * @param configurationData A structure that will be filled with the
   * configuration data. If the adapter has an active session, the data is
   * already loaded, so it's instantaneous.
   * \warning Returns `true` if `configurationData` has been filled, or `false`
   * if there's no active session.
   */
  [[nodiscard]] bool readConfiguration(ConfigurationData& configurationData) {
    if (!isSessionActive())
      return false;

    configurationData = adapterConfiguration.fields;
    return true;
  }

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

  /**
   * @brief Returns the current role in the P2P connection.
   * @return One of the enum values from `LinkMobile::Role`.
   */
  [[nodiscard]] Role getRole() { return role; }

  /**
   * @brief Returns whether the adapter has been configured or not.
   * @return 1 = yes, 0 = no, -1 = unknown (no session active).
   */
  [[nodiscard]] int isConfigurationValid() {
    if (!isSessionActive())
      return -1;

    return (int)adapterConfiguration.isValid();
  }

  /**
   * @brief Returns `true` if a P2P call is established (the state is
   * `CALL_ESTABLISHED`).
   */
  [[nodiscard]] bool isConnectedP2P() {
    return state == State::CALL_ESTABLISHED;
  }

  /**
   * @brief Returns `true` if a PPP session is active (the state is
   * `PPP_ACTIVE`).
   */
  [[nodiscard]] bool isConnectedPPP() { return state == State::PPP_ACTIVE; }

  /**
   * @brief Returns `true` if the session is active.
   */
  [[nodiscard]] bool isSessionActive() {
    return state >= State::SESSION_ACTIVE && state <= State::SHUTDOWN_REQUESTED;
  }

  /**
   * @brief Returns `true` if there's an active session and there's no previous
   * shutdown requests.
   */
  [[nodiscard]] bool canShutdown() {
    return isSessionActive() && state != State::SHUTDOWN_REQUESTED;
  }

  /**
   * @brief Returns the current operation mode (`LinkSPI::DataSize`).
   */
  [[nodiscard]] LinkSPI::DataSize getDataSize() {
    return linkSPI.getDataSize();
  }

  /**
   * @brief Returns details about the last error that caused the connection to
   * be aborted.
   */
  [[nodiscard]] Error getError() { return error; }

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

    if (shouldAbortOnStateTimeout()) {
      timeoutStateFrames++;
      if (timeoutStateFrames >= INIT_TIMEOUT_FRAMES)
        return abort(Error::Type::ADAPTER_NOT_CONNECTED);
    }

    pingFrameCount++;
    if (pingFrameCount >= PING_FREQUENCY_FRAMES && isSessionActive() &&
        !asyncCommand.isActive) {
      pingFrameCount = 0;
      cmdTelephoneStatus();
    }

    processUserRequests();
    processNewFrame();
  }

  /**
   * @brief This method is called by the SERIAL interrupt handler.
   * \warning This is internal API!
   */
  void _onSerial() {
    if (!isEnabled)
      return;

    linkSPI._onSerial();
    u32 newData = linkSPI.getAsyncData();

    if (state == State::NEEDS_RESET)
      return;

    if (asyncCommand.isActive) {
      if (asyncCommand.state == AsyncCommand::State::PENDING) {
        if (isSIO32Mode()) {
          if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
            sendAsyncCommandSIO32(newData);
          else
            receiveAsyncCommandSIO32(newData);
        } else {
          if (asyncCommand.direction == AsyncCommand::Direction::SENDING)
            sendAsyncCommandSIO8(newData);
          else
            receiveAsyncCommandSIO8(newData);
        }

        if (asyncCommand.state == AsyncCommand::State::COMPLETED) {
          asyncCommand.isActive = false;
          processAsyncCommand();
        }
      }
    } else {
      processLoosePacket(newData);
    }
  }

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

    linkSPI.transferAsync(pendingTransfer);
    stopTimer();
    hasPendingTransfer = false;
  }

  struct Config {
    u32 timeout;
    u8 timerId;
  };

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

 private:
  enum class AdapterType { BLUE, YELLOW, GREEN, RED, UNKNOWN };

  struct UserRequest {
    enum class Type {
      CALL,
      PPP_LOGIN,
      DNS_QUERY,
      OPEN_CONNECTION,
      CLOSE_CONNECTION,
      TRANSFER,
      HANG_UP,
      SHUTDOWN
    };

    Type type;
    char phoneNumber[LINK_MOBILE_MAX_PHONE_NUMBER_LENGTH + 1];
    char loginId[LINK_MOBILE_MAX_LOGIN_ID_LENGTH + 1];
    char password[LINK_MOBILE_MAX_PASSWORD_LENGTH + 1];
    DNSQuery* dns;
    OpenConn* open;
    CloseConn* close;
    u8 ip[4];
    u16 port;
    ConnectionType connectionType;
    u8 connectionId;
    DataTransfer send;
    DataTransfer* receive;
    bool commandSent;
    u32 timeout;
    bool finished;

    void cleanup() {
      if (finished)
        return;
      AsyncRequest* metadata =
          type == UserRequest::Type::DNS_QUERY          ? (AsyncRequest*)dns
          : type == UserRequest::Type::OPEN_CONNECTION  ? (AsyncRequest*)open
          : type == UserRequest::Type::CLOSE_CONNECTION ? (AsyncRequest*)close
          : type == UserRequest::Type::TRANSFER         ? (AsyncRequest*)receive
                                                        : nullptr;
      if (metadata != nullptr) {
        metadata->success = false;
        metadata->completed = true;
      }
    }
  };

  union AdapterConfiguration {
    ConfigurationData fields;
    char bytes[CONFIGURATION_DATA_SIZE];

    bool isValid() {
      return fields.magic[0] == 'M' && fields.magic[1] == 'A' &&
             (fields.registrationState & 1) == 1 &&
             calculatedChecksum() == reportedChecksum();
    }

    u16 calculatedChecksum() {
      u16 result = 0;
      for (u32 i = 0; i < CONFIGURATION_DATA_SIZE - 2; i++)
        result += bytes[i];
      return result;
    }

    u16 reportedChecksum() {
      return Link::buildU16(fields.checksumHigh, fields.checksumLow);
    }
  };

  struct MagicBytes {
    u8 magic1 = COMMAND_MAGIC_VALUE1;
    u8 magic2 = COMMAND_MAGIC_VALUE2;
  } LINK_PACKED;

  struct PacketData {
    u8 bytes[LINK_MOBILE_COMMAND_TRANSFER_BUFFER] = {};
  } LINK_PACKED;

  struct PacketHeader {
    u8 commandId = 0;
    u8 _unused_ = 0;
    u8 _unusedSizeHigh_ = 0;
    u8 size = 0;

    u16 sum() { return commandId + _unused_ + _unusedSizeHigh_ + size; }
    u8 pureCommandId() { return commandId & (~OR_VALUE); }
  } LINK_PACKED;

  struct PacketChecksum {
    u8 high = 0;
    u8 low = 0;
  } LINK_PACKED;

  struct Command {
    MagicBytes magicBytes;
    PacketHeader header;
    PacketData data;
    PacketChecksum checksum;
  };

  struct CommandResponse {
    CommandResult result = CommandResult::PENDING;
    Command command;
  };

  struct AsyncCommand {
    enum class State { PENDING, COMPLETED };
    enum class Direction { SENDING, RECEIVING };

    State state;
    CommandResult result;
    u32 transferred;
    Command cmd;
    Direction direction;
    u16 expectedChecksum;
    u8 errorCommandId;
    u8 errorCode;
    bool isActive = false;

    void reset() {
      state = AsyncCommand::State::PENDING;
      result = CommandResult::PENDING;
      transferred = 0;
      cmd = Command{};
      direction = AsyncCommand::Direction::SENDING;
      expectedChecksum = 0;
      errorCommandId = 0;
      errorCode = 0;
      isActive = false;
    }

    u8 relatedCommandId() {
      return result == CommandResult::ERROR_CODE ? errorCommandId
                                                 : cmd.header.pureCommandId();
    }

    bool respondsTo(u8 commandId) {
      return direction == AsyncCommand::Direction::RECEIVING &&
             (result == CommandResult::ERROR_CODE
                  ? errorCommandId == commandId
                  : cmd.header.commandId == (commandId | OR_VALUE));
    }

    void finish() {
      if (cmd.header.commandId == COMMAND_ERROR_STATUS) {
        if (cmd.header.size != 2) {
          result = CommandResult::WEIRD_ERROR_CODE;
        } else {
          result = CommandResult::ERROR_CODE;
          errorCommandId = cmd.data.bytes[0];
          errorCode = cmd.data.bytes[1];
        }
      } else {
        result = CommandResult::SUCCESS;
      }

      state = AsyncCommand::State::COMPLETED;
    }

    void fail(CommandResult _result) {
      result = _result;
      state = AsyncCommand::State::COMPLETED;
    }
  };

  static constexpr u32 PREAMBLE_SIZE =
      sizeof(MagicBytes) + sizeof(PacketHeader);
  static constexpr u32 CHECKSUM_SIZE = sizeof(PacketChecksum);

  using RequestQueue = Link::Queue<UserRequest, LINK_MOBILE_QUEUE_SIZE>;

  LinkSPI linkSPI;
  RequestQueue userRequests;
  AdapterConfiguration adapterConfiguration;
  AsyncCommand asyncCommand;
  u32 waitFrames = 0;
  u32 timeoutStateFrames = 0;
  u32 pingFrameCount = 0;
  volatile Role role = Role::NO_P2P_CONNECTION;
  volatile State state = State::NEEDS_RESET;
  PacketData nextCommandData;
  u32 nextCommandDataSize = 0;
  bool hasPendingTransfer = false;
  u32 pendingTransfer = 0;
  AdapterType adapterType = AdapterType::UNKNOWN;
  Error error = {};
  volatile bool isEnabled = false;

  void processUserRequests() {
    if (!userRequests.canMutate() || userRequests.isEmpty())
      return;

    if (!isSessionActive()) {
      userRequests.clear();
      return;
    }

    if (userRequests.peek().finished)
      userRequests.pop();
    if (userRequests.isEmpty())
      return;

    auto request = userRequests.peek();
    request.timeout++;
    if (shouldAbortOnRequestTimeout() && request.timeout >= config.timeout)
      return abort(Error::Type::TIMEOUT);

    switch (request.type) {
      case UserRequest::Type::CALL: {
        if (state != State::SESSION_ACTIVE && state != State::CALL_REQUESTED) {
          popRequest();
          return;
        }
        if (state != State::CALL_REQUESTED)
          setState(State::CALL_REQUESTED);

        if (!asyncCommand.isActive) {
          setState(State::CALLING);
          cmdDialTelephone(request.phoneNumber);
          popRequest();
        }
        break;
      }
      case UserRequest::Type::PPP_LOGIN: {
        if (state != State::SESSION_ACTIVE &&
            state != State::ISP_CALL_REQUESTED && state != State::ISP_CALLING) {
          popRequest();
          return;
        }
        if (state == State::SESSION_ACTIVE)
          setState(State::ISP_CALL_REQUESTED);

        if (!asyncCommand.isActive && state == State::ISP_CALL_REQUESTED) {
          setState(State::ISP_CALLING);
          cmdDialTelephone(adapterConfiguration.isValid()
                               ? adapterConfiguration.fields._ispNumber1
                               : FALLBACK_ISP_NUMBER);
        }
        break;
      }
      case UserRequest::Type::DNS_QUERY: {
        if (state != State::PPP_ACTIVE) {
          popRequest();
          return;
        }

        if (!asyncCommand.isActive && !request.commandSent) {
          cmdDNSQuery(request.send.data, request.send.size);
          request.commandSent = true;
        }
        break;
      }
      case UserRequest::Type::OPEN_CONNECTION: {
        if (state != State::PPP_ACTIVE) {
          popRequest();
          return;
        }

        if (!asyncCommand.isActive && !request.commandSent) {
          if (request.connectionType == ConnectionType::TCP)
            cmdOpenTCPConnection(request.ip, request.port);
          else
            cmdOpenUDPConnection(request.ip, request.port);
          request.commandSent = true;
        }
        break;
      }
      case UserRequest::Type::CLOSE_CONNECTION: {
        if (state != State::PPP_ACTIVE) {
          popRequest();
          return;
        }

        if (!asyncCommand.isActive && !request.commandSent) {
          if (request.connectionType == ConnectionType::TCP)
            cmdCloseTCPConnection(request.connectionId);
          else
            cmdCloseUDPConnection(request.connectionId);
          request.commandSent = true;
        }
        break;
      }
      case UserRequest::Type::TRANSFER: {
        if (state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) {
          popRequest();
          return;
        }

        if (!asyncCommand.isActive && !request.commandSent) {
          cmdTransferData(request.connectionId, request.send.data,
                          request.send.size);
          request.commandSent = true;
        }
        break;
      }
      case UserRequest::Type::HANG_UP: {
        if (state != State::CALL_ESTABLISHED && state != State::PPP_ACTIVE) {
          popRequest();
          return;
        }
        if (!asyncCommand.isActive)
          cmdHangUpTelephone();
        break;
      }
      case UserRequest::Type::SHUTDOWN: {
        if (state != State::SHUTDOWN_REQUESTED)
          setState(State::SHUTDOWN_REQUESTED);

        if (!asyncCommand.isActive) {
          setState(State::ENDING_SESSION);
          cmdEndSession();
          popRequest();
        }
        break;
      }
      default: {
      }
    }
  }

  void processNewFrame() {
    switch (state) {
      case State::WAITING_TO_START: {
        waitFrames--;

        if (waitFrames == 0) {
          setState(State::STARTING_SESSION);
          cmdBeginSession();
        }
        break;
      }
      case State::WAITING_32BIT_SWITCH: {
        waitFrames--;

        if (waitFrames == 0) {
          setState(State::READING_CONFIGURATION);
          cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK);
        }
        break;
      }
      case State::SESSION_ACTIVE: {
        if (!asyncCommand.isActive)
          cmdWaitForTelephoneCall();

        break;
      }
      case State::WAITING_8BIT_SWITCH: {
        waitFrames--;

        if (waitFrames == 0) {
          error = {};
          setState(State::SHUTDOWN);
        }
        break;
      }
      default: {
      }
    }
  }

  void processAsyncCommand() {
    if (asyncCommand.result != CommandResult::SUCCESS) {
      if (shouldAbortOnCommandFailure())
        return abort(Error::Type::COMMAND_FAILED);
      else
        abort(Error::Type::COMMAND_FAILED, false);  // (log the error)
    }

    _LMLOG_("%s $%X [%d]",
            asyncCommand.direction == AsyncCommand::Direction::SENDING ? ">!"
                                                                       : "<!",
            asyncCommand.cmd.header.pureCommandId(),
            asyncCommand.cmd.header.size);

    if (asyncCommand.direction == AsyncCommand::Direction::SENDING) {
      receiveCommandAsync();
      return;
    }

    if (asyncCommand.respondsTo(COMMAND_TELEPHONE_STATUS)) {
      if (asyncCommand.cmd.header.size != 3)
        return abort(Error::Type::WEIRD_RESPONSE);
      if (state == State::CALL_ESTABLISHED) {
        if (!isBitHigh(asyncCommand.cmd.data.bytes[0], 2)) {
          // (call terminated)
          setState(State::SESSION_ACTIVE);
        }
      }
      return;
    }

    switch (state) {
      case State::STARTING_SESSION: {
        if (!asyncCommand.respondsTo(COMMAND_BEGIN_SESSION))
          return;
        if (asyncCommand.cmd.header.size != LOGIN_PARTS_SIZE)
          return abort(Error::Type::WEIRD_RESPONSE);

        for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++) {
          if (asyncCommand.cmd.data.bytes[i] != LOGIN_PARTS[i])
            return abort(Error::Type::WEIRD_RESPONSE);
        }

        setState(State::ACTIVATING_SIO32);
        cmdSIO32(true);
        break;
      }
      case State::ACTIVATING_SIO32: {
        if (asyncCommand.respondsTo(COMMAND_RESET)) {
          // If the adapter responds to a 0x16 instead of 0x18,
          // it's libmobile telling us that SIO32 is not supported.
          // In that case, we continue using SIO8.
          setState(State::READING_CONFIGURATION);
          cmdReadConfigurationData(0, CONFIGURATION_DATA_CHUNK);
          return;
        }
        if (!asyncCommand.respondsTo(COMMAND_SIO32))
          return;

        setState(State::WAITING_32BIT_SWITCH);
        waitFrames = INIT_WAIT_FRAMES;
        linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS,
                         LinkSPI::DataSize::SIZE_32BIT);
        break;
      }
      case State::READING_CONFIGURATION: {
        if (!asyncCommand.respondsTo(COMMAND_READ_CONFIGURATION_DATA))
          return;

        u32 offset = asyncCommand.cmd.data.bytes[0];
        u32 sizeWithOffsetByte = asyncCommand.cmd.header.size;
        if (asyncCommand.result != CommandResult::SUCCESS ||
            sizeWithOffsetByte != CONFIGURATION_DATA_CHUNK + 1 ||
            (offset != 0 && offset != CONFIGURATION_DATA_CHUNK))
          return abort(Error::Type::WEIRD_RESPONSE);

        for (u32 i = 0; i < CONFIGURATION_DATA_CHUNK; i++)
          adapterConfiguration.bytes[offset + i] =
              asyncCommand.cmd.data.bytes[1 + i];

        if (offset == 0) {
          cmdReadConfigurationData(CONFIGURATION_DATA_CHUNK,
                                   CONFIGURATION_DATA_CHUNK);
        } else {
          setISPNumber();
          setState(State::SESSION_ACTIVE);
        }
        break;
      }
      case State::SESSION_ACTIVE: {
        if (!asyncCommand.respondsTo(COMMAND_WAIT_FOR_TELEPHONE_CALL))
          return;

        if (asyncCommand.result == CommandResult::SUCCESS) {
          setState(State::CALL_ESTABLISHED);
          role = Role::RECEIVER;
        } else {
          // (no call received)
        }
        break;
      }
      case State::CALLING: {
        if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE))
          return;

        if (asyncCommand.result == CommandResult::SUCCESS) {
          setState(State::CALL_ESTABLISHED);
          role = Role::CALLER;
        } else {
          // (call failed)
          setState(State::SESSION_ACTIVE);
        }
        break;
      }
      case State::CALL_ESTABLISHED: {
        if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) {
          setState(State::SESSION_ACTIVE);
          return;
        }
        if (!asyncCommand.respondsTo(COMMAND_TRANSFER_DATA))
          return;
        if (userRequests.isEmpty())
          return abort(Error::Type::WTF);
        auto request = userRequests.peekRef();

        handleTransferDataResponse(request);

        break;
      }
      case State::ISP_CALLING: {
        if (!asyncCommand.respondsTo(COMMAND_DIAL_TELEPHONE))
          return;
        if (userRequests.isEmpty())
          return abort(Error::Type::WTF);
        auto request = userRequests.peekRef();
        if (request->type != UserRequest::Type::PPP_LOGIN)
          return abort(Error::Type::WTF);

        if (asyncCommand.result == CommandResult::SUCCESS) {
          setState(State::PPP_LOGIN);
          cmdISPLogin(request->loginId, request->password);
        } else {
          // (ISP call failed)
          setState(State::SESSION_ACTIVE);
        }
        request->finished = true;

        break;
      }
      case State::PPP_LOGIN: {
        if (!asyncCommand.respondsTo(COMMAND_ISP_LOGIN))
          return;
        if (asyncCommand.result != CommandResult::SUCCESS)
          return abort(Error::Type::PPP_LOGIN_FAILED);

        setState(State::PPP_ACTIVE);
        break;
      }
      case State::PPP_ACTIVE: {
        if (asyncCommand.respondsTo(COMMAND_HANG_UP_TELEPHONE)) {
          setState(State::SESSION_ACTIVE);
          return;
        }

        if (userRequests.isEmpty())
          return;
        auto request = userRequests.peekRef();

        if (asyncCommand.respondsTo(COMMAND_DNS_QUERY)) {
          if (request->type != UserRequest::Type::DNS_QUERY)
            return abort(Error::Type::WTF);

          if (asyncCommand.result == CommandResult::SUCCESS) {
            if (asyncCommand.cmd.header.size != 4)
              return abort(Error::Type::WEIRD_RESPONSE);
            for (u32 i = 0; i < 4; i++)
              request->dns->ipv4[i] = asyncCommand.cmd.data.bytes[i];
            request->dns->success = true;
          } else {
            request->dns->success = false;
          }

          request->dns->completed = true;
          request->finished = true;
        } else if (asyncCommand.respondsTo(COMMAND_OPEN_TCP_CONNECTION) ||
                   asyncCommand.respondsTo(COMMAND_OPEN_UDP_CONNECTION)) {
          if (request->type != UserRequest::Type::OPEN_CONNECTION)
            return abort(Error::Type::WTF);

          if (asyncCommand.result == CommandResult::SUCCESS) {
            if (asyncCommand.cmd.header.size != 1)
              return abort(Error::Type::WEIRD_RESPONSE);
            request->open->connectionId = asyncCommand.cmd.data.bytes[0];
            request->open->success = true;
          } else {
            request->open->success = false;
          }

          request->open->completed = true;
          request->finished = true;
        } else if (asyncCommand.respondsTo(COMMAND_CLOSE_TCP_CONNECTION) ||
                   asyncCommand.respondsTo(COMMAND_CLOSE_UDP_CONNECTION)) {
          if (request->type != UserRequest::Type::CLOSE_CONNECTION)
            return abort(Error::Type::WTF);

          request->close->success =
              asyncCommand.result == CommandResult::SUCCESS;
          request->close->completed = true;
          request->finished = true;
        } else if (asyncCommand.respondsTo(COMMAND_TRANSFER_DATA)) {
          if (request->type != UserRequest::Type::TRANSFER)
            return abort(Error::Type::WTF);

          handleTransferDataResponse(request);
        } else if (asyncCommand.respondsTo(COMMAND_CONNECTION_CLOSED)) {
          if (request->type != UserRequest::Type::TRANSFER)
            return abort(Error::Type::WTF);

          // (connection closed)
          request->receive->success = false;
          request->receive->completed = true;
          request->finished = true;
        }
        break;
      }
      case State::ENDING_SESSION: {
        if (!asyncCommand.respondsTo(COMMAND_END_SESSION))
          return;

        setState(State::WAITING_8BIT_SWITCH);
        waitFrames = INIT_WAIT_FRAMES;
        linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS,
                         LinkSPI::DataSize::SIZE_8BIT);
        break;
      }
      default: {
      }
    }
  }

  void handleTransferDataResponse(UserRequest* request) {
    if (request->type != UserRequest::Type::TRANSFER)
      return abort(Error::Type::WTF);

    if (asyncCommand.result == CommandResult::SUCCESS) {
      if (asyncCommand.cmd.header.size == 0)
        return abort(Error::Type::WEIRD_RESPONSE);

      u32 size = asyncCommand.cmd.header.size - 1;
      for (u32 i = 0; i < size; i++)
        request->receive->data[i] = asyncCommand.cmd.data.bytes[1 + i];

      request->receive->data[size] = '\0';
      // (just for convenience when using strings, the buffer is big enough)

      request->receive->size = size;
      request->receive->success = true;
    } else {
      request->receive->success = false;
    }

    request->receive->completed = true;
    request->finished = true;
  }

  void processLoosePacket(u32 newData) {
    switch (state) {
      case State::PINGING: {
        setState(State::WAITING_TO_START);
        waitFrames = INIT_WAIT_FRAMES;
        break;
      }
      default: {
      }
    }
  }

  void cmdBeginSession() {
    for (u32 i = 0; i < LOGIN_PARTS_SIZE; i++)
      addData(LOGIN_PARTS[i], i == 0);
    sendCommandAsync(buildCommand(COMMAND_BEGIN_SESSION, true));
  }

  void cmdEndSession() { sendCommandAsync(buildCommand(COMMAND_END_SESSION)); }

  void cmdDialTelephone(const char* phoneNumber) {
    addData(DIAL_PHONE_FIRST_BYTE[(int)adapterType], true);
    for (u32 i = 0; i < Link::strlen(phoneNumber); i++)
      addData(phoneNumber[i]);
    sendCommandAsync(buildCommand(COMMAND_DIAL_TELEPHONE, true));
  }

  void cmdHangUpTelephone() {
    sendCommandAsync(buildCommand(COMMAND_HANG_UP_TELEPHONE, true));
  }

  void cmdWaitForTelephoneCall() {
    sendCommandAsync(buildCommand(COMMAND_WAIT_FOR_TELEPHONE_CALL));
  }

  void cmdTransferData(u8 connectionId, const u8* data, u8 size) {
    addData(connectionId, true);
    for (u32 i = 0; i < size; i++)
      addData(data[i]);
    sendCommandAsync(buildCommand(COMMAND_TRANSFER_DATA, true));
  }

  void cmdTelephoneStatus() {
    sendCommandAsync(buildCommand(COMMAND_TELEPHONE_STATUS));
  }

  void cmdSIO32(bool enabled) {
    addData(enabled, true);
    sendCommandAsync(buildCommand(COMMAND_SIO32, true));
  }

  void cmdReadConfigurationData(u8 offset, u8 size) {
    addData(offset, true);
    addData(CONFIGURATION_DATA_CHUNK);
    sendCommandAsync(buildCommand(COMMAND_READ_CONFIGURATION_DATA, true));
  }

  void cmdISPLogin(const char* loginId, const char* password) {
    u32 loginIdLength = Link::strlen(loginId);
    addData(loginIdLength, true);
    for (u32 i = 0; i < loginIdLength; i++)
      addData(loginId[i]);

    u32 passwordLength = Link::strlen(password);
    addData(passwordLength);
    for (u32 i = 0; i < passwordLength; i++)
      addData(password[i]);

    bool isConfigured = isConfigurationValid();
    for (u32 i = 0; i < 4; i++)
      addData(isConfigured ? adapterConfiguration.fields.primaryDNS[i] : 0);
    for (u32 i = 0; i < 4; i++)
      addData(isConfigured ? adapterConfiguration.fields.secondaryDNS[i] : 0);

    sendCommandAsync(buildCommand(COMMAND_ISP_LOGIN, true));
  }

  void cmdOpenTCPConnection(const u8* ip, u16 port) {
    for (u32 i = 0; i < 4; i++)
      addData(ip[i], i == 0);
    addData(Link::msB16(port));
    addData(Link::lsB16(port));
    sendCommandAsync(buildCommand(COMMAND_OPEN_TCP_CONNECTION, true));
  }

  void cmdCloseTCPConnection(u8 connectionId) {
    addData(connectionId);
    sendCommandAsync(buildCommand(COMMAND_CLOSE_TCP_CONNECTION, true));
  }

  void cmdOpenUDPConnection(const u8* ip, u16 port) {
    for (u32 i = 0; i < 4; i++)
      addData(ip[i], i == 0);
    addData(Link::msB16(port));
    addData(Link::lsB16(port));
    sendCommandAsync(buildCommand(COMMAND_OPEN_UDP_CONNECTION, true));
  }

  void cmdCloseUDPConnection(u8 connectionId) {
    addData(connectionId);
    sendCommandAsync(buildCommand(COMMAND_CLOSE_UDP_CONNECTION, true));
  }

  void cmdDNSQuery(const u8* data, u8 size) {
    for (u32 i = 0; i < size; i++)
      addData(data[i], i == 0);
    sendCommandAsync(buildCommand(COMMAND_DNS_QUERY, true));
  }

  void setISPNumber() {
    static const char BCD[16] = "0123456789#*cde";

    for (u32 i = 0; i < 8; i++) {
      u8 b = adapterConfiguration.fields.configurationSlot1[i];
      adapterConfiguration.fields._ispNumber1[i * 2] = BCD[b >> 4];
      adapterConfiguration.fields._ispNumber1[i * 2 + 1] = BCD[b & 0xF];
    }
  }

  void pushRequest(UserRequest request) {
    request.timeout = 0;
    request.finished = false;
    userRequests.syncPush(request);
  }

  void popRequest() {
    auto request = userRequests.peekRef();
    request->cleanup();
    request->finished = true;
    userRequests.pop();
  }

  bool shouldAbortOnStateTimeout() {
    return state > State::NEEDS_RESET && state < State::SESSION_ACTIVE;
  }

  bool shouldAbortOnRequestTimeout() { return true; }

  bool shouldAbortOnCommandFailure() {
    u8 commandId = asyncCommand.relatedCommandId();
    return asyncCommand.direction == AsyncCommand::Direction::SENDING ||
           (commandId != COMMAND_WAIT_FOR_TELEPHONE_CALL &&
            commandId != COMMAND_DIAL_TELEPHONE && !isAsyncRequest(commandId));
  }

  bool isAsyncRequest(u8 commandId) {
    return commandId == COMMAND_DNS_QUERY ||
           commandId == COMMAND_OPEN_TCP_CONNECTION ||
           commandId == COMMAND_CLOSE_TCP_CONNECTION ||
           commandId == COMMAND_OPEN_UDP_CONNECTION ||
           commandId == COMMAND_CLOSE_UDP_CONNECTION ||
           commandId == COMMAND_TRANSFER_DATA;
  }

  void addData(u8 value, bool start = false) {
    if (start) {
      nextCommandDataSize = 0;
      nextCommandData = PacketData{};
    }
    nextCommandData.bytes[nextCommandDataSize] = value;
    nextCommandDataSize++;
  }

  void copyString(char* target, const char* source, u32 length) {
    u32 len = Link::strlen(source);

    for (u32 i = 0; i < length + 1; i++)
      if (i < len)
        target[i] = source[i];
      else
        target[i] = '\0';
  }

  void setState(State newState) {
    role = Role::NO_P2P_CONNECTION;
    State oldState = state;
    state = newState;
    timeoutStateFrames = 0;
    pingFrameCount = 0;
    _LMLOG_("!! new state: %d -> %d", oldState, newState);
    (void)oldState;
  }

  void abort(Error::Type errorType, bool fatal = true) {
    auto newError = Error{
        .type = errorType,
        .state = state,
        .cmdId = asyncCommand.relatedCommandId(),
        .cmdResult = asyncCommand.result,
        .cmdErrorCode = asyncCommand.errorCode,
        .cmdIsSending =
            asyncCommand.direction == AsyncCommand::Direction::SENDING,

        .reqType = userRequests.isEmpty() ? -1 : (int)userRequests.peek().type};

    _LMLOG_(
        "!! %s:\n  error: %d\n  cmdId: %s$%X\n  cmdResult: %d\n  "
        "cmdErrorCode: %d",
        fatal ? "aborted" : "failed", newError.type,
        newError.cmdIsSending ? ">" : "<", newError.cmdId, newError.cmdResult,
        newError.cmdErrorCode);
    (void)newError;

    if (fatal) {
      error = newError;
      resetState();
      stop();
    }
  }

  void reset() {
    resetState();
    stop();
    start();
  }

  void resetState() {
    LINK_BARRIER;
    setState(State::NEEDS_RESET);

    adapterConfiguration = AdapterConfiguration{};
    userRequests.clear();
    asyncCommand.reset();
    waitFrames = 0;
    timeoutStateFrames = 0;
    role = Role::NO_P2P_CONNECTION;
    nextCommandDataSize = 0;
    hasPendingTransfer = false;
    pendingTransfer = 0;
    adapterType = AdapterType::UNKNOWN;

    userRequests.syncClear();
    LINK_BARRIER;
  }

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

  void start() {
    linkSPI.activate(LinkSPI::Mode::MASTER_256KBPS,
                     LinkSPI::DataSize::SIZE_8BIT);

    setState(State::PINGING);
    transferAsync(0);
  }

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

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

  void sendCommandAsync(Command command) {
    _LMLOG_(">> $%X [%d] (...)", command.header.commandId, command.header.size);
    asyncCommand.reset();
    asyncCommand.cmd = command;
    asyncCommand.isActive = true;

    if (isSIO32Mode())  // Magic+Header
      advance32(
          Link::buildU32(command.magicBytes.magic1, command.magicBytes.magic2,
                         command.header.commandId, command.header._unused_));
    else  // Magic Bytes (1)
      advance8(command.magicBytes.magic1);
  }

  void receiveCommandAsync() {
    _LMLOG_("<< ...");
    asyncCommand.reset();
    asyncCommand.direction = AsyncCommand::Direction::RECEIVING;
    asyncCommand.isActive = true;

    if (isSIO32Mode())
      transferAsync(GBA_WAITING_32BIT);
    else
      transferAsync(GBA_WAITING);
  }

  void sendAsyncCommandSIO8(u32 newData) {
    const u8* commandBytes = (const u8*)&asyncCommand.cmd;
    u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size;

    if (asyncCommand.transferred < mainSize) {
      // Magic Bytes (2) + Packet Header + Packet Data
      advance8(commandBytes[asyncCommand.transferred]);
    } else if (asyncCommand.transferred < mainSize + CHECKSUM_SIZE) {
      // Packet Checksum
      commandBytes += PREAMBLE_SIZE + LINK_MOBILE_COMMAND_TRANSFER_BUFFER;
      advance8(commandBytes[asyncCommand.transferred - mainSize]);
    } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) {
      // Acknowledgement Signal (1)
      advance8(DEVICE_GBA | OR_VALUE);
    } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) {
      // Acknowledgement Signal (2)
      if (!isSupportedAdapter(newData))
        return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
      advance8(ACK_SENDER);
    } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 2) {
      // Acknowledgement Signal (3)
      if (newData != (asyncCommand.cmd.header.commandId ^ OR_VALUE))
        return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
      asyncCommand.finish();
    }
  }

  void sendAsyncCommandSIO32(u32 newData) {
    u32 dataSize = asyncCommand.cmd.header.size;
    u32 alignment = dataSize % 4;
    u32 padding = alignment != 0 ? 4 - alignment : 0;
    u32 mainSize = PREAMBLE_SIZE + dataSize + padding;

    if (asyncCommand.transferred == 4) {
      // Header+Data || Header+Checksum
      advance32(dataSize > 0
                    ? Link::buildU32(asyncCommand.cmd.header._unusedSizeHigh_,
                                     asyncCommand.cmd.header.size,
                                     asyncCommand.cmd.data.bytes[0],
                                     asyncCommand.cmd.data.bytes[1])
                    : Link::buildU32(asyncCommand.cmd.header._unusedSizeHigh_,
                                     asyncCommand.cmd.header.size,
                                     asyncCommand.cmd.checksum.high,
                                     asyncCommand.cmd.checksum.low));
    } else if (asyncCommand.transferred < mainSize) {
      // Data || Data+Checksum
      u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE;
      u32 pendingDataCount = (dataSize + padding) - transferredDataCount;
      advance32(pendingDataCount > 2
                    ? Link::buildU32(
                          asyncCommand.cmd.data.bytes[transferredDataCount],
                          asyncCommand.cmd.data.bytes[transferredDataCount + 1],
                          asyncCommand.cmd.data.bytes[transferredDataCount + 2],
                          asyncCommand.cmd.data.bytes[transferredDataCount + 3])
                    : Link::buildU32(
                          asyncCommand.cmd.data.bytes[transferredDataCount],
                          asyncCommand.cmd.data.bytes[transferredDataCount + 1],
                          asyncCommand.cmd.checksum.high,
                          asyncCommand.cmd.checksum.low));
    } else if (asyncCommand.transferred < mainSize + 4) {
      // Acknowledgement Signal (1)
      advance32(Link::buildU32(DEVICE_GBA | OR_VALUE, ACK_SENDER, 0, 0));
    } else {
      // Acknowledgement Signal (2)
      u16 ackData = Link::msB32(newData);
      if (!isSupportedAdapter(Link::msB16(ackData)))
        return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
      if (Link::lsB16(ackData) !=
          (asyncCommand.cmd.header.commandId ^ OR_VALUE))
        return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
      asyncCommand.finish();
    }
  }

  void receiveAsyncCommandSIO8(u32 newData) {
    u8* commandBytes = (u8*)&asyncCommand.cmd;
    u32 mainSize = PREAMBLE_SIZE + asyncCommand.cmd.header.size;

    if (asyncCommand.transferred == 0) {
      // Magic Bytes (1)
      if (newData == ADAPTER_WAITING)
        return transferAsync(GBA_WAITING);
      if (newData != COMMAND_MAGIC_VALUE1)
        return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
      advance8(GBA_WAITING);
    } else if (asyncCommand.transferred == 1) {
      // Magic Bytes (1)
      if (newData != COMMAND_MAGIC_VALUE2)
        return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
      advance8(GBA_WAITING);
    } else if (asyncCommand.transferred < PREAMBLE_SIZE) {
      // Packet Header
      commandBytes[asyncCommand.transferred] = newData;
      if (asyncCommand.cmd.header._unusedSizeHigh_ != 0 ||
          asyncCommand.cmd.header.size >
              LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH)
        return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE);
      advance8(GBA_WAITING);
      if (asyncCommand.transferred == PREAMBLE_SIZE)
        asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum();
    } else if (asyncCommand.transferred < mainSize) {
      // Packet Data
      commandBytes[asyncCommand.transferred] = newData;
      asyncCommand.expectedChecksum += newData;
      advance8(GBA_WAITING);
    } else if (asyncCommand.transferred == mainSize) {
      // Packet Checksum (1)
      if (newData != Link::msB16(asyncCommand.expectedChecksum))
        return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
      advance8(GBA_WAITING);
    } else if (asyncCommand.transferred == mainSize + 1) {
      // Packet Checksum (2)
      if (newData != Link::lsB16(asyncCommand.expectedChecksum))
        return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
      advance8(DEVICE_GBA | OR_VALUE);
    } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE) {
      // Acknowledgement Signal (1)
      if (!isSupportedAdapter(newData))
        return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
      advance8(asyncCommand.cmd.header.commandId ^ OR_VALUE);
    } else if (asyncCommand.transferred == mainSize + CHECKSUM_SIZE + 1) {
      // Acknowledgement Signal (2)
      if (newData != ACK_SENDER)
        return asyncCommand.fail(CommandResult::INVALID_COMMAND_ACK);
      asyncCommand.finish();
    }
  }

  void receiveAsyncCommandSIO32(u32 newData) {
    u32 dataSize = asyncCommand.cmd.header.size;
    u32 alignment = dataSize % 4;
    u32 padding = alignment != 0 ? 4 - alignment : 0;
    u32 mainSize = PREAMBLE_SIZE + dataSize + padding;

    if (asyncCommand.transferred == 0) {
      // Magic+Header
      if (newData == ADAPTER_WAITING || newData == ADAPTER_WAITING_32BIT)
        return transferAsync(GBA_WAITING_32BIT);
      u16 magic = Link::msB32(newData);
      u16 firstHalfHeader = Link::lsB32(newData);
      if (Link::msB16(magic) != COMMAND_MAGIC_VALUE1 ||
          Link::lsB16(magic) != COMMAND_MAGIC_VALUE2)
        return asyncCommand.fail(CommandResult::INVALID_MAGIC_BYTES);
      asyncCommand.cmd.header.commandId = Link::msB16(firstHalfHeader);
      asyncCommand.cmd.header._unused_ = Link::lsB16(firstHalfHeader);
      advance32(GBA_WAITING_32BIT);
    } else if (asyncCommand.transferred == 4) {
      // Header+Data || Header+Checksum
      u16 secondHalfHeader = Link::msB32(newData);
      asyncCommand.cmd.header._unusedSizeHigh_ = Link::msB16(secondHalfHeader);
      asyncCommand.cmd.header.size = Link::lsB16(secondHalfHeader);
      if (asyncCommand.cmd.header._unusedSizeHigh_ != 0 ||
          asyncCommand.cmd.header.size >
              LINK_MOBILE_MAX_COMMAND_TRANSFER_LENGTH)
        return asyncCommand.fail(CommandResult::WEIRD_DATA_SIZE);
      asyncCommand.expectedChecksum = asyncCommand.cmd.header.sum();
      if (asyncCommand.cmd.header.size > 0) {
        u16 firstData = Link::lsB32(newData);
        u8 b0 = Link::msB16(firstData), b1 = Link::lsB16(firstData);
        asyncCommand.cmd.data.bytes[0] = b0;
        asyncCommand.cmd.data.bytes[1] = b1;
        asyncCommand.expectedChecksum += b0 + b1;
      } else {
        u16 checksum = Link::lsB32(newData);
        if (checksum != asyncCommand.expectedChecksum)
          return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
        asyncCommand.cmd.checksum.high = Link::msB16(checksum);
        asyncCommand.cmd.checksum.low = Link::lsB16(checksum);
      }
      advance32(GBA_WAITING_32BIT);
    } else if (asyncCommand.transferred < mainSize) {
      // Data || Data+Checksum
      u32 transferredDataCount = asyncCommand.transferred - PREAMBLE_SIZE;
      u32 pendingDataCount = (dataSize + padding) - transferredDataCount;
      if (pendingDataCount > 2) {
        u16 dataHigh = Link::msB32(newData);
        u16 dataLow = Link::lsB32(newData);
        u8 b0 = Link::msB16(dataHigh), b1 = Link::lsB16(dataHigh),
           b2 = Link::msB16(dataLow), b3 = Link::lsB16(dataLow);
        asyncCommand.cmd.data.bytes[transferredDataCount] = b0;
        asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1;
        asyncCommand.cmd.data.bytes[transferredDataCount + 2] = b2;
        asyncCommand.cmd.data.bytes[transferredDataCount + 3] = b3;
        asyncCommand.expectedChecksum += b0 + b1 + b2 + b3;
        advance32(GBA_WAITING_32BIT);
      } else {
        u16 lastData = Link::msB32(newData);
        u8 b0 = Link::msB16(lastData), b1 = Link::lsB16(lastData);
        asyncCommand.cmd.data.bytes[transferredDataCount] = b0;
        asyncCommand.cmd.data.bytes[transferredDataCount + 1] = b1;
        asyncCommand.expectedChecksum += b0 + b1;
        u16 checksum = Link::lsB32(newData);
        if (checksum != asyncCommand.expectedChecksum)
          return asyncCommand.fail(CommandResult::WRONG_CHECKSUM);
        asyncCommand.cmd.checksum.high = Link::msB16(checksum);
        asyncCommand.cmd.checksum.low = Link::lsB16(checksum);
        advance32(Link::buildU32(DEVICE_GBA | OR_VALUE,
                                 asyncCommand.cmd.header.commandId ^ OR_VALUE,
                                 0, 0));
      }
    } else {
      // Acknowledgement Signal
      u32 ackData = Link::msB32(newData);
      if (!isSupportedAdapter(Link::msB16(ackData)) ||
          Link::lsB16(ackData) != ACK_SENDER)
        return asyncCommand.fail(CommandResult::INVALID_DEVICE_ID);
      asyncCommand.finish();
    }
  }

  bool isSupportedAdapter(u8 ack) {
    for (u32 i = 0; i < SUPPORTED_DEVICES_SIZE; i++) {
      if ((SUPPORTED_DEVICES[i] | OR_VALUE) == ack) {
        if (adapterType == AdapterType::UNKNOWN)
          adapterType = static_cast<AdapterType>(i);

        return true;
      }
    }

    return false;
  }

  Command buildCommand(u8 type, bool withData = false) {
    Command command;
    command.header.commandId = type;
    command.header._unused_ = 0;
    command.header._unusedSizeHigh_ = 0;
    command.header.size = withData ? (u8)nextCommandDataSize : 0;
    if (withData)
      command.data = nextCommandData;
    u16 checksum = command.header.sum();
    for (u32 i = 0; i < command.header.size; i++)
      checksum += command.data.bytes[i];
    command.checksum.high = Link::msB16(checksum);
    command.checksum.low = Link::lsB16(checksum);

    return command;
  }

  void advance8(u32 data) {
    transferAsync(data);
    asyncCommand.transferred++;
  }

  void advance32(u32 data) {
    transferAsync(data);
    asyncCommand.transferred += 4;
  }

  void transferAsync(u32 data) {
    hasPendingTransfer = true;
    pendingTransfer = data;
    startTimer(WAIT_TICKS[isSIO32Mode()]);
  }

  bool isSIO32Mode() {
    return linkSPI.getDataSize() == LinkSPI::DataSize::SIZE_32BIT;
  }

  bool isBitHigh(u8 byte, u8 bit) { return (byte >> bit) & 1; }
};

extern LinkMobile* linkMobile;

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

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

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

#undef _LMLOG_

#endif  // LINK_MOBILE_H
