#define LINK_RAW_WIRELESS_ENABLE_LOGGING

#include "../../../../lib/LinkRawWireless.hpp"

#include "DebugScene.h"

#include <libgba-sprite-engine/background/text_stream.h>
#include <algorithm>
#include <functional>
#include "../../../_lib/common.h"
#include "../../../_lib/libgba-sprite-engine/scene.h"

DebugScene::DebugScene(std::shared_ptr<GBAEngine> engine) : Scene(engine) {}

static std::unique_ptr<InputHandler> aHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> bHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> upHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> downHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> lHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> rHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> selectHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> startHandler =
    std::unique_ptr<InputHandler>(new InputHandler());
static std::unique_ptr<InputHandler> rightHandler =
    std::unique_ptr<InputHandler>(new InputHandler());

static std::vector<std::string> logLines;
static u32 currentLogLine = 0;
static bool useVerboseLog = true;

static const std::string CHARACTERS[] = {
    "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c",
    "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p",
    "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "A", "B", "C",
    "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P",
    "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z"};
static const u32 CHARACTERS_LEN = 62;

#define MAX_LINES 18
#define DRAW_LINE 2

void printScrollableText(u32 currentLine,
                         std::vector<std::string> lines,
                         bool withCursor = false) {
  for (u32 i = 0; i < MAX_LINES; i++) {
    u32 lastLineIndex = MAX_LINES - 1;
    u32 index = max(currentLine, lastLineIndex) - lastLineIndex + i;
    if (index < lines.size()) {
      std::string cursor = currentLine == index ? "> " : "  ";
      TextStream::instance().setText((withCursor ? cursor : "") + lines[index],
                                     DRAW_LINE + i, -3);
    } else {
      TextStream::instance().setText("                              ",
                                     DRAW_LINE + i, -3);
    }
  }
}

void print() {
  printScrollableText(currentLogLine, logLines);
}

void scrollBack() {
  if (currentLogLine <= 0)
    return;
  currentLogLine--;
  print();
}

void scrollForward() {
  if (currentLogLine < MAX_LINES - 1)
    currentLogLine = min(MAX_LINES - 1, logLines.size() - 1);
  if (currentLogLine == logLines.size() - 1)
    return;
  currentLogLine++;
  print();
}

void scrollPageUp() {
  currentLogLine = max(currentLogLine - MAX_LINES, 0);
  print();
}

void scrollPageDown() {
  currentLogLine = min(currentLogLine + MAX_LINES, logLines.size() - 1);
  print();
}

void scrollToTop() {
  currentLogLine = 0;
  print();
}

void scrollToBottom() {
  currentLogLine = logLines.size() - 1;
  print();
}

void clear() {
  logLines.clear();
  currentLogLine = 0;
  print();
}

void log(std::string string) {
  logLines.push_back(string);
  scrollPageDown();
}

const u32* toArray(std::vector<u32> vector) {
  return vector.data();
}

std::vector<Background*> DebugScene::backgrounds() {
  return {};
}

std::vector<Sprite*> DebugScene::sprites() {
  std::vector<Sprite*> sprites;
  return sprites;
}

void DebugScene::load() {
  SCENE_init();
  BACKGROUND_enable(true, false, false, false);

  linkRawWireless->logger = [](std::string string) {
    if (useVerboseLog)
      log(string);
  };

  log("---");
  log("LinkRawWireless_demo");
  log("  (v8.0.0)");
  log("");
  log("START: reset wireless adapter");
  log("RIGHT: restore from multiboot");
  log("A: send command");
  log("B: toggle log level");
  log("UP/DOWN: scroll up/down");
  log("L/R: scroll page up/down");
  log("UP+L/DOWN+R: scroll to top/bottom");
  log("SELECT: clear");
  log("---");
  log("");
  toggleLogLevel();

  addCommandMenuOptions();
  for (u32 i = 0; i < 4; i++)
    serverIds[i] = 0;
}

void DebugScene::tick(u16 keys) {
  if (engine->isTransitioning())
    return;

  std::string state =
      linkRawWireless->getState() == LinkRawWireless::State::NEEDS_RESET
          ? "NEEDS_RESET       "
      : linkRawWireless->getState() == LinkRawWireless::State::AUTHENTICATED
          ? "AUTHENTICATED     "
      : linkRawWireless->getState() == LinkRawWireless::State::SEARCHING
          ? "SEARCHING         "
      : linkRawWireless->getState() == LinkRawWireless::State::SERVING
          ? "SERVING           "
      : linkRawWireless->getState() == LinkRawWireless::State::CONNECTING
          ? "CONNECTING        "
      : linkRawWireless->getState() == LinkRawWireless::State::CONNECTED
          ? "CONNECTED         "
          : "?                 ";
  TextStream::instance().setText(
      "state = " + state + "p" +
          std::to_string(linkRawWireless->sessionState.currentPlayerId) +
          (linkRawWireless->getState() == LinkRawWireless::State::SERVING
               ? "/" + std::to_string(linkRawWireless->sessionState.playerCount)
               : ""),
      0, -3);

  processKeys(keys);
  processButtons();

  __qran_seed += keys;
  __qran_seed += REG_RCNT;
  __qran_seed += REG_SIOCNT;
}

void DebugScene::addCommandMenuOptions() {
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x10 (Hello)", .command = 0x10});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x11 (SignalLevel)", .command = 0x11});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x12 (VersionStatus)", .command = 0x12});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x13 (SystemStatus)", .command = 0x13});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x14 (SlotStatus)", .command = 0x14});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x15 (ConfigStatus)", .command = 0x15});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x16 (Broadcast)", .command = 0x16});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x17 (Setup)", .command = 0x17});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x18 (?)", .command = 0x18});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x19 (StartHost)", .command = 0x19});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1A (PollConnections)", .command = 0x1A});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1B (EndHost)", .command = 0x1B});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1C (BroadcastReadStart)", .command = 0x1C});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1D (BroadcastReadPoll)", .command = 0x1D});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1E (BroadcastReadEnd)", .command = 0x1E});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x1F (Connect)", .command = 0x1F});
  commandMenuOptions.push_back(CommandMenuOption{
      .name = "0x20 (IsConnectionComplete)", .command = 0x20});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x21 (FinishConnection)", .command = 0x21});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x24 (SendData)", .command = 0x24});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x25 (SendDataAndWait)", .command = 0x25});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x26 (ReceiveData)", .command = 0x26});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x27 (Wait)", .command = 0x27});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x30 (DisconnectClient)", .command = 0x30});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x32 (?)", .command = 0x32});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x33 (?)", .command = 0x33});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x34 (?)", .command = 0x34});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x35 (?!)", .command = 0x35});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x37 (RetransmitAndWait)", .command = 0x37});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x38 (?)", .command = 0x38});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x39 (?)", .command = 0x39});
  commandMenuOptions.push_back(
      CommandMenuOption{.name = "0x3D (Bye)", .command = 0x3D});
}

void DebugScene::processKeys(u16 keys) {
  aHandler->setIsPressed(keys & KEY_A);
  bHandler->setIsPressed(keys & KEY_B);
  upHandler->setIsPressed(keys & KEY_UP);
  downHandler->setIsPressed(keys & KEY_DOWN);
  lHandler->setIsPressed(keys & KEY_L);
  rHandler->setIsPressed(keys & KEY_R);
  selectHandler->setIsPressed(keys & KEY_SELECT);
  startHandler->setIsPressed(keys & KEY_START);
  rightHandler->setIsPressed(keys & KEY_RIGHT);
}

void DebugScene::processButtons() {
  if (bHandler->hasBeenPressedNow())
    toggleLogLevel();

  if (aHandler->hasBeenPressedNow()) {
    std::vector<std::string> commandNames;
    commandNames.resize(commandMenuOptions.size());
    std::transform(commandMenuOptions.begin(), commandMenuOptions.end(),
                   commandNames.begin(),
                   [](CommandMenuOption x) { return x.name; });

    int selectedCommandIndex =
        selectOption("Which command?", commandNames, lastSelectedCommandIndex);
    if (selectedCommandIndex > -1) {
      lastSelectedCommandIndex = selectedCommandIndex;
      processCommand((u32)selectedCommandIndex);
    }

    print();
  }

  if (lHandler->hasBeenPressedNow()) {
    if (upHandler->getIsPressed())
      scrollToTop();
    else
      scrollPageUp();
  }

  if (rHandler->hasBeenPressedNow()) {
    if (downHandler->getIsPressed())
      scrollToBottom();
    else
      scrollPageDown();
  }

  if (upHandler->getIsPressed())
    scrollBack();

  if (downHandler->getIsPressed())
    scrollForward();

  if (selectHandler->hasBeenPressedNow())
    clear();

  if (startHandler->hasBeenPressedNow())
    resetAdapter();

  if (rightHandler->hasBeenPressedNow())
    restoreExistingConnection();
}

void DebugScene::toggleLogLevel() {
  if (useVerboseLog) {
    useVerboseLog = false;
    log("! setting log level to NORMAL");
  } else {
    useVerboseLog = true;
    log("! setting log level to VERBOSE");
  }
  log("");
}

int DebugScene::selectOption(std::string title,
                             std::vector<std::string> options,
                             u32 cursor = 0) {
  u32 selectedOption = cursor;
  bool firstTime = true;

  while (true) {
    u16 keys = ~REG_KEYS & KEY_ANY;
    processKeys(keys);

    u32 oldOption = selectedOption;

    if (lHandler->hasBeenPressedNow()) {
      if (upHandler->getIsPressed())
        selectedOption = 0;
      else
        selectedOption = max(selectedOption - MAX_LINES, 0);
    }
    if (rHandler->hasBeenPressedNow()) {
      if (downHandler->getIsPressed())
        selectedOption = options.size() - 1;
      else
        selectedOption = min(selectedOption + MAX_LINES, options.size() - 1);
    }
    if (downHandler->hasBeenPressedNow() && selectedOption < options.size() - 1)
      selectedOption++;
    if (upHandler->hasBeenPressedNow() && selectedOption > 0)
      selectedOption--;

    if (firstTime || selectedOption != oldOption) {
      TextStream::instance().setText(title, 0, -3);
      printScrollableText(selectedOption, options, true);
    }

    if (bHandler->hasBeenPressedNow())
      return -1;

    if (aHandler->hasBeenPressedNow())
      return selectedOption;

    VBlankIntrWait();
    firstTime = false;
  }
}

std::string DebugScene::selectString(u32 maxCharacters) {
  std::vector<std::string> options = {"<end>"};
  for (u32 i = 0; i < CHARACTERS_LEN; i++)
    options.push_back(CHARACTERS[i]);

again:
  std::string str;
  for (u32 i = 0; i < maxCharacters; i++) {
    int characterIndex = -1;
    if ((characterIndex =
             selectOption("Next character? (" + str + ")", options)) == -1) {
      if (i == 0)
        return "";
      else
        goto again;
    }
    if (characterIndex == 0)
      break;
    str += CHARACTERS[characterIndex - 1];
  }

  if (str == "")
    goto again;

  if (selectOption(">> " + str + "?", std::vector<std::string>{"yes", "no"}) ==
      1)
    goto again;

  return str;
}

int DebugScene::selectU32(std::string title) {
again0:
  int byte0 = selectU8(title + " - Byte 0 (0x000000XX)");
  if (byte0 == -1)
    return -1;
again1:
  int byte1 = selectU8(title + " - Byte 1 (0x0000XX" +
                       linkRawWireless->toHex(byte0, 2) + ")");
  if (byte1 == -1)
    goto again0;
again2:
  int byte2 =
      selectU8(title + " - Byte 2 (0x00XX" + linkRawWireless->toHex(byte1, 2) +
               linkRawWireless->toHex(byte0, 2) + ")");
  if (byte2 == -1)
    goto again1;
  int byte3 =
      selectU8(title + " - Byte 3 (0xXX" + linkRawWireless->toHex(byte2, 2) +
               linkRawWireless->toHex(byte1, 2) +
               linkRawWireless->toHex(byte0, 2) + ")");
  if (byte3 == -1)
    goto again2;

  u16 numberLow = Link::buildU16((u8)byte1, (u8)byte0);
  u16 numberHigh = Link::buildU16((u8)byte3, (u8)byte2);
  u32 number = Link::buildU32(numberHigh, numberLow);
  if (selectOption(">> 0x" + linkRawWireless->toHex(number, 8) + "?",
                   std::vector<std::string>{"yes", "no"}) == 1)
    goto again0;

  return number;
}

int DebugScene::selectU16() {
again:
  int lsB = selectU8("Choose lsB (0x00XX)");
  if (lsB == -1)
    return -1;
  int msB = selectU8("Choose msB (0xXX" + linkRawWireless->toHex(lsB, 2) + ")");
  if (msB == -1)
    goto again;

  u16 number = Link::buildU16((u8)msB, (u8)lsB);
  if (selectOption(">> 0x" + linkRawWireless->toHex(number, 4) + "?",
                   std::vector<std::string>{"yes", "no"}) == 1)
    goto again;

  return number;
}

int DebugScene::selectU8(std::string title) {
  std::vector<std::string> options;
  for (u32 i = 0; i < 1 << 8; i++)
    options.push_back(linkRawWireless->toHex(i, 2));
  return selectOption(title, options);
}

void DebugScene::processCommand(u32 selectedCommandIndex) {
  CommandMenuOption selectedOption = commandMenuOptions[selectedCommandIndex];
  auto name = selectedOption.name;
  auto command = selectedOption.command;

  if (selectHandler->getIsPressed())
    goto generic;

  switch (command) {
    case 0x10:
    case 0x11:
      return logOperation("sending " + name, []() {
        LinkRawWireless::SignalLevelResponse response;
        bool success = linkRawWireless->getSignalLevel(response);

        if (success) {
          log("< [levelH] " + std::to_string(response.signalLevels[0]));
          log("< [levelC0] " + std::to_string(response.signalLevels[1]));
          log("< [levelC1] " + std::to_string(response.signalLevels[2]));
          log("< [levelC2] " + std::to_string(response.signalLevels[3]));
          log("< [levelC3] " + std::to_string(response.signalLevels[4]));
        }

        return success;
      });
    case 0x12:
      goto simple;
    case 0x13: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::SystemStatusResponse response;
        bool success = linkRawWireless->getSystemStatus(response);

        if (success) {
          log("< [device id] " + linkRawWireless->toHex(response.deviceId, 4));
          log("< [player id] " + std::to_string(response.currentPlayerId));
          log("< [state] " + std::to_string((int)response.adapterState));
          log("< [closed] " + std::to_string(response.isServerClosed));
        }

        return success;
      });
    }
    case 0x14: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::SlotStatusResponse response;
        bool success = linkRawWireless->getSlotStatus(response);

        if (success) {
          log("< [next slot] " +
              linkRawWireless->toHex(response.nextClientNumber, 2));
          for (u32 i = 0; i < response.connectedClientsSize; i++) {
            log("< [client" +
                std::to_string(response.connectedClients[i].clientNumber) +
                "] " +
                linkRawWireless->toHex(response.connectedClients[i].deviceId,
                                       4));
          }
        }

        return success;
      });
    }
    case 0x15:
      goto simple;
    case 0x16: {
      int gameId = selectGameId();
      if (gameId == -1)
        return;
      std::string gameName = selectGameName();
      if (gameName == "")
        return;
      std::string userName = selectUserName();
      if (userName == "")
        return;

      return logOperation("sending " + name, [gameName, userName, gameId]() {
        log("setting broadcast:");
        log("[room.gameId] " + linkRawWireless->toHex(gameId, 4));
        log("[room.game] " + gameName);
        log("[room.user] " + userName);

        bool success = linkRawWireless->broadcast(
            gameName.c_str(), userName.c_str(), (u16)gameId);

        if (success)
          log("NOW CALL 0x19!");

        return success;
      });
    }
    case 0x17: {
      int maxPlayers = -1;
      while ((maxPlayers = selectOption(
                  "Max players?",
                  std::vector<std::string>{"5", "4", "3", "2"})) == -1)
        ;
      int maxTransmissions = -1;
      while ((maxTransmissions = selectU8("Max transmissions?")) == -1)
        ;
      int waitTimeout = -1;
      while ((waitTimeout = selectU8("Wait timeout?")) == -1)
        ;

      return logOperation(
          "sending " + name, [maxPlayers, maxTransmissions, waitTimeout]() {
            log("maxPlayers = " + std::to_string(maxPlayers));
            log("maxTransmissions = " + std::to_string(maxTransmissions));
            log("waitTimeout = " + std::to_string(waitTimeout));

            return linkRawWireless->setup(5 - maxPlayers, maxTransmissions,
                                          waitTimeout);
          });
    }
    case 0x18:
      goto generic;
    case 0x19: {
      return logOperation("sending " + name,
                          []() { return linkRawWireless->startHost(); });
    }
    case 0x1A: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::PollConnectionsResponse response;
        bool success = linkRawWireless->pollConnections(response);

        if (success) {
          for (u32 i = 0; i < response.connectedClientsSize; i++) {
            log("< [client" +
                std::to_string(response.connectedClients[i].clientNumber) +
                "] " +
                linkRawWireless->toHex(response.connectedClients[i].deviceId,
                                       4));
          }
        }

        return success;
      });
    }
    case 0x1B: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::PollConnectionsResponse response;
        bool success = linkRawWireless->endHost(response);

        if (success) {
          for (u32 i = 0; i < response.connectedClientsSize; i++) {
            log("< [client" +
                std::to_string(response.connectedClients[i].clientNumber) +
                "] " +
                linkRawWireless->toHex(response.connectedClients[i].deviceId,
                                       4));
          }
        }

        return success;
      });
    }
    case 0x1C: {
      return logOperation("sending " + name, []() {
        bool success = linkRawWireless->broadcastReadStart();

        if (success)
          log("NOW CALL 0x1D!");

        return success;
      });
    }
    case 0x1D: {
      return logOperation("sending " + name, [this]() {
        LinkRawWireless::BroadcastReadPollResponse response;
        bool success = linkRawWireless->broadcastReadPoll(response);

        if (success) {
          auto servers = response.servers;
          for (u32 i = 0; i < response.serversSize; i++) {
            serverIds[i] = servers[i].id;

            log("< [room" + std::to_string(i) + ".id] " +
                linkRawWireless->toHex(servers[i].id, 4));
            log("< [room" + std::to_string(i) + ".gameId] " +
                linkRawWireless->toHex(servers[i].gameId, 4));
            log("< [room" + std::to_string(i) + ".game] " +
                servers[i].gameName);
            log("< [room" + std::to_string(i) + ".user] " +
                servers[i].userName);
            log("< [room" + std::to_string(i) + ".nextSlot] " +
                linkRawWireless->toHex(servers[i].nextClientNumber, 2));
          }

          if (response.serversSize > 0)
            log("NOW CALL 0x1E!");
          else
            log("No rooms? NOW CALL 0x1E!");
        }

        return success;
      });
    }
    case 0x1E: {
      return logOperation("sending " + name, []() {
        bool success = linkRawWireless->broadcastReadEnd();

        if (success)
          log("NOW CALL 0x1F!");

        return success;
      });
    }
    case 0x1F: {
      u16 serverId = selectServerId();
      if (serverId == -1)
        return;

      return logOperation("sending " + name, [serverId]() {
        log("connecting to " + linkRawWireless->toHex(serverId, 4));

        bool success = linkRawWireless->connect(serverId);

        if (success)
          log("NOW CALL 0x20!");

        return success;
      });
    }
    case 0x20: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::ConnectionStatus response;
        bool success = linkRawWireless->keepConnecting(response);

        if (success) {
          log(std::string("< [phase] ") +
              (response.phase ==
                       LinkRawWireless::ConnectionPhase::STILL_CONNECTING
                   ? "CONNECTING"
               : response.phase == LinkRawWireless::ConnectionPhase::ERROR
                   ? "ERROR"
                   : "SUCCESS"));
          if (response.phase == LinkRawWireless::ConnectionPhase::SUCCESS)
            log("< [slot] " + std::to_string(response.assignedClientNumber));

          log("NOW CALL 0x21!");
        }

        return success;
      });
    }
    case 0x21: {
      return logOperation("sending " + name,
                          []() { return linkRawWireless->finishConnection(); });
    }
    case 0x24: {
      auto data = selectDataToSend();
      if (data.empty())
        return;
      u32 bytes = data[0];
      data.erase(data.begin());

      return logOperation("sending " + name, [&data, bytes]() {
        return linkRawWireless->sendData(toArray(data), data.size(), bytes);
      });
    }
    case 0x25: {
      auto data = selectDataToSend();
      if (data.empty())
        return;

      if (selectOption("What mode?",
                       std::vector<std::string>{"sync", "async"}) == 1) {
        return logOperation("sending " + name, [&data]() {
          u32 bytes = data[0];
          data[0] = linkRawWireless->getSendDataHeaderFor(bytes);

          bool success = linkRawWireless->sendCommandAsync(0x25, toArray(data),
                                                           data.size(), true);
          if (!success) {
            log("! not now");
            return false;
          }

          while (linkRawWireless->getAsyncState() ==
                 LinkRawWireless::AsyncState::WORKING)
            ;

          auto result = linkRawWireless->getAsyncCommandResult();

          if (result.success) {
            log("< [notif] " + linkRawWireless->toHex(result.commandId));
            for (u32 i = 0; i < result.dataSize; i++) {
              log("< [param" + std::to_string(i) + "] " +
                  linkRawWireless->toHex(result.data[i]));
            }
          }

          return success;
        });
      } else {
        u32 bytes = data[0];
        data.erase(data.begin());

        return logOperation("sending " + name, [&data, bytes]() {
          LinkRawWireless::CommandResult remoteCommand;
          bool success = linkRawWireless->sendDataAndWait(
              toArray(data), data.size(), remoteCommand, bytes);

          if (success) {
            log("< [notif] " + linkRawWireless->toHex(remoteCommand.commandId));
            for (u32 i = 0; i < remoteCommand.dataSize; i++) {
              log("< [param" + std::to_string(i) + "] " +
                  linkRawWireless->toHex(remoteCommand.data[i]));
            }
          }

          return success;
        });
      }
    }
    case 0x26: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::ReceiveDataResponse response;
        bool success = linkRawWireless->receiveData(response);

        if (success) {
          log("< [bytesH] " + std::to_string(response.sentBytes[0]));
          log("< [bytesC0] " + std::to_string(response.sentBytes[1]));
          log("< [bytesC1] " + std::to_string(response.sentBytes[2]));
          log("< [bytesC2] " + std::to_string(response.sentBytes[3]));
          log("< [bytesC3] " + std::to_string(response.sentBytes[4]));

          for (u32 i = 0; i < response.dataSize; i++)
            log("< [data" + std::to_string(i) + "] " +
                linkRawWireless->toHex(response.data[i]));
        }

        return success;
      });
    }
    case 0x27: {
      return logOperation("sending " + name, []() {
        LinkRawWireless::CommandResult remoteCommand;
        bool success = linkRawWireless->wait(remoteCommand);

        if (success) {
          log("< [notif] " + linkRawWireless->toHex(remoteCommand.commandId));
          for (u32 i = 0; i < remoteCommand.dataSize; i++) {
            log("< [param" + std::to_string(i) + "] " +
                linkRawWireless->toHex(remoteCommand.data[i]));
          }
        }

        return success;
      });
    }
    case 0x30: {
      bool client0 = selectOption("Disconnect client 0?",
                                  std::vector<std::string>{"yes", "no"}) == 0;
      bool client1 = selectOption("Disconnect client 1?",
                                  std::vector<std::string>{"yes", "no"}) == 0;
      bool client2 = selectOption("Disconnect client 2?",
                                  std::vector<std::string>{"yes", "no"}) == 0;
      bool client3 = selectOption("Disconnect client 3?",
                                  std::vector<std::string>{"yes", "no"}) == 0;

      return logOperation("sending " + name,
                          [client0, client1, client2, client3]() {
                            bool success = linkRawWireless->disconnectClient(
                                client0, client1, client2, client3);
                            if (success)
                              log("DISCONNECTED!");
                            return success;
                          });
    }
    case 0x32:
    case 0x33:
    case 0x34:
      goto generic;
    case 0x35:
    case 0x37:
      goto genericWait;
    case 0x38:
    case 0x39:
      goto generic;
    case 0x3D:
      goto simple;
    default:
      return;
  }

simple:
  return logSimpleCommand(name, command);
generic:
  return logGenericCommand(name, command);
genericWait:
  return logGenericWaitCommand(name, command);
}

int DebugScene::selectServerId() {
  switch (selectOption("Which server ID?", std::vector<std::string>{
                                               "<first>", "<second>", "<third>",
                                               "<fourth>", "<pick>"})) {
    case 0: {
      if (serverIds[0] == 0)
        return -1;

      return serverIds[0];
    }
    case 1: {
      if (serverIds[1] == 0)
        return -1;

      return serverIds[1];
    }
    case 2: {
      if (serverIds[2] == 0)
        return -1;

      return serverIds[2];
    }
    case 3: {
      if (serverIds[3] == 0)
        return -1;

      return serverIds[3];
    }
    default: {
      return selectU16();
    }
  }
}

int DebugScene::selectGameId() {
  switch (selectOption(
      "GameID?",
      std::vector<std::string>{"0x7FFF", "0x1234", "<random>", "<pick>"})) {
    case 0: {
      return 0x7FFF;
    }
    case 1: {
      return 0x1234;
    }
    case 2: {
      return Link::buildU16(qran_range(0, 256), qran_range(0, 256));
    }
    default: {
      return selectU16();
    }
  }
}

std::string DebugScene::selectGameName() {
  switch (selectOption("Game name?",
                       std::vector<std::string>{"LinkConnection", "<pick>"})) {
    case 0: {
      return "LinkConnection";
    }
    default: {
      return selectString(LINK_RAW_WIRELESS_MAX_GAME_NAME_LENGTH);
    }
  }
}

std::vector<u32> DebugScene::selectDataToSend() {
  std::vector<u32> data;
  std::vector<std::string> options;

  for (u32 i = 0; i < 87; i++)
    options.push_back(std::to_string(i));
  int bytes = selectOption("How many BYTES?", options);
  if (bytes == -1)
    return data;

  data.push_back(bytes);

  if (selectOption(">> Include data?", std::vector<std::string>{"yes", "no"}) ==
      1)
    return data;

  u32 words = (bytes + 3) / 4;
  for (u32 i = 0; i < (u32)words; i++) {
    int word = selectU32("Word " + std::to_string(i + 1) + "/" +
                         std::to_string(words));
    if (word == -1)
      return data;
    data.push_back(word);
  }

  return data;
}

std::vector<u32> DebugScene::selectData() {
  std::vector<u32> data;

  std::vector<std::string> options;
  for (u32 i = 0; i < LINK_RAW_WIRELESS_MAX_COMMAND_TRANSFER_LENGTH; i++)
    options.push_back(std::to_string(i));
  int words = selectOption("How many words?", options);
  if (words == -1)
    return data;

  for (u32 i = 0; i < (u32)words; i++) {
    int word = selectU32("Word " + std::to_string(i + 1) + "/" +
                         std::to_string(words));
    if (word == -1)
      return data;
    data.push_back(word);
  }

  return data;
}

std::string DebugScene::selectUserName() {
  switch (
      selectOption("User name?", std::vector<std::string>{"Demo", "<pick>"})) {
    case 0: {
      return "Demo";
    }
    default: {
      return selectString(LINK_RAW_WIRELESS_MAX_USER_NAME_LENGTH);
    }
  }
}

void DebugScene::logGenericWaitCommand(std::string name, u32 id) {
  auto data = selectData();
  return logOperation("sending " + name, [id, &data]() {
    auto result =
        linkRawWireless->sendCommand(id, toArray(data), data.size(), true);
    for (u32 i = 0; i < result.dataSize; i++) {
      log("< [response" + std::to_string(i) + "] " +
          linkRawWireless->toHex(result.data[i]));
    }

    if (!result.success)
      return false;

    log("Now WAITING...");

    LinkRawWireless::CommandResult remoteCommand =
        linkRawWireless->receiveCommandFromAdapter();

    if (remoteCommand.success) {
      log("< [notif] " + linkRawWireless->toHex(remoteCommand.commandId));
      for (u32 i = 0; i < remoteCommand.dataSize; i++) {
        log("< [param" + std::to_string(i) + "] " +
            linkRawWireless->toHex(remoteCommand.data[i]));
      }
    }

    return remoteCommand.success;
  });
}

void DebugScene::logGenericCommand(std::string name, u32 id) {
  auto data = selectData();
  return logSimpleCommand(name, id, data);
}

void DebugScene::logSimpleCommand(std::string name,
                                  u32 id,
                                  std::vector<u32> params) {
  logOperation("sending " + name, [id, &params]() {
    auto result =
        linkRawWireless->sendCommand(id, toArray(params), params.size());
    for (u32 i = 0; i < result.dataSize; i++) {
      log("< [response" + std::to_string(i) + "] " +
          linkRawWireless->toHex(result.data[i]));
    }
    return result.success;
  });
}

void DebugScene::logOperation(std::string name,
                              std::function<bool()> operation) {
  log("> " + name + "...");
  bool success = operation();
  log(success ? "< success :)" : "< failure :(");
  log("");
}

void DebugScene::resetAdapter() {
  logOperation("resetting adapter",
               []() { return linkRawWireless->activate(); });
}

void DebugScene::restoreExistingConnection() {
  logOperation("restoring from multiboot",
               []() { return linkRawWireless->restoreExistingConnection(); });
}
