#include "StaticGameData.hh"

#include <array>

#include "Text.hh"

using namespace std;

bool episode_has_arpg_semantics(Episode ep) {
  return (ep == Episode::EP1) || (ep == Episode::EP2) || (ep == Episode::EP4);
}

const char* name_for_episode(Episode ep) {
  switch (ep) {
    case Episode::NONE:
      return "No episode";
    case Episode::EP1:
      return "Episode 1";
    case Episode::EP2:
      return "Episode 2";
    case Episode::EP3:
      return "Episode 3";
    case Episode::EP4:
      return "Episode 4";
    default:
      return "Unknown episode";
  }
}

const char* token_name_for_episode(Episode ep) {
  switch (ep) {
    case Episode::EP1:
      return "Episode1";
    case Episode::EP2:
      return "Episode2";
    case Episode::EP3:
      return "Episode3";
    case Episode::EP4:
      return "Episode4";
    default:
      throw logic_error("invalid episode");
  }
}

Episode episode_for_token_name(const string& name) {
  if (name == "Episode1") {
    return Episode::EP1;
  }
  if (name == "Episode2") {
    return Episode::EP2;
  }
  if (name == "Episode3") {
    return Episode::EP3;
  }
  if (name == "Episode4") {
    return Episode::EP4;
  }
  throw runtime_error("unknown episode");
}

Episode episode_for_area(uint8_t area) {
  if (area < 0x12) {
    return Episode::EP1;
  } else if (area < 0x24) {
    return Episode::EP2;
  } else if (area < 0x2F) {
    return Episode::EP4;
  } else {
    throw std::runtime_error("invalid area number");
  }
}

const char* abbreviation_for_episode(Episode ep) {
  switch (ep) {
    case Episode::NONE:
      return "None";
    case Episode::EP1:
      return "Ep1";
    case Episode::EP2:
      return "Ep2";
    case Episode::EP3:
      return "Ep3";
    case Episode::EP4:
      return "Ep4";
    default:
      return "UnkEp";
  }
}

const char* name_for_mode(GameMode mode) {
  switch (mode) {
    case GameMode::NORMAL:
      return "Normal";
    case GameMode::BATTLE:
      return "Battle";
    case GameMode::CHALLENGE:
      return "Challenge";
    case GameMode::SOLO:
      return "Solo";
    default:
      return "Unknown mode";
  }
}

const char* abbreviation_for_mode(GameMode mode) {
  switch (mode) {
    case GameMode::NORMAL:
      return "Nml";
    case GameMode::BATTLE:
      return "Btl";
    case GameMode::CHALLENGE:
      return "Chl";
    case GameMode::SOLO:
      return "Solo";
    default:
      return "UnkMd";
  }
}

static const array<const char*, 10> section_id_to_name = {
    "Viridia", "Greennill", "Skyly", "Bluefull", "Purplenum", "Pinkal", "Redria", "Oran", "Yellowboze", "Whitill"};

static const array<const char*, 10> section_id_to_abbreviation = {
    "Vir", "Grn", "Sky", "Blu", "Prp", "Pnk", "Red", "Orn", "Ylw", "Wht"};

const unordered_map<string, uint8_t> name_to_section_id({{"viridia", 0},
    // Greennill is spelled Greenill in some places, so we accept both spellings
    {"greennill", 1}, {"greenill", 1}, {"skyly", 2}, {"bluefull", 3}, {"purplenum", 4}, {"pinkal", 5}, {"redria", 6},
    {"oran", 7}, {"yellowboze", 8}, {"whitill", 9},

    // Shortcuts for chat commands
    {"b", 3}, {"g", 1}, {"o", 7}, {"pi", 5}, {"pu", 4}, {"r", 6}, {"s", 2}, {"v", 0}, {"w", 9}, {"y", 8}});

const vector<string> lobby_event_to_name = {
    "none", "xmas", "none", "val", "easter", "hallo", "sonic", "newyear",
    "summer", "white", "wedding", "fall", "s-spring", "s-summer", "spring"};

const unordered_map<string, uint8_t> name_to_lobby_event = {
    {"none", 0}, {"xmas", 1}, {"val", 3}, {"easter", 4}, {"hallo", 5}, {"sonic", 6}, {"newyear", 7}, {"summer", 8},
    {"white", 9}, {"wedding", 10}, {"fall", 11}, {"s-spring", 12}, {"s-summer", 13}, {"spring", 14}};

const unordered_map<uint8_t, string> lobby_type_to_name = {
    {0x00, "normal"}, {0x0F, "inormal"}, {0x10, "ipc"}, {0x11, "iball"}, {0x67, "cave2u"}, {0xD4, "cave1"},
    {0xE9, "planet"}, {0xEA, "clouds"}, {0xED, "cave"}, {0xEE, "jungle"}, {0xEF, "forest2-2"}, {0xF0, "forest2-1"},
    {0xF1, "windpower"}, {0xF2, "overview"}, {0xF3, "seaside"}, {0xF4, "fons"}, {0xF5, "dmorgue"}, {0xF6, "caelum"},
    {0xF8, "cyber"}, {0xF9, "boss1"}, {0xFA, "boss2"}, {0xFB, "dolor"}, {0xFC, "dragon"}, {0xFD, "derolle"},
    {0xFE, "volopt"}, {0xFF, "darkfalz"}};

const unordered_map<string, uint8_t> name_to_lobby_type = {
    {"normal", 0x00}, {"inormal", 0x0F}, {"ipc", 0x10}, {"iball", 0x11}, {"cave1", 0xD4}, {"cave2u", 0x67},
    {"dragon", 0xFC}, {"derolle", 0xFD}, {"volopt", 0xFE}, {"darkfalz", 0xFF}, {"planet", 0xE9}, {"clouds", 0xEA},
    {"cave", 0xED}, {"jungle", 0xEE}, {"forest2-2", 0xEF}, {"forest2-1", 0xF0}, {"windpower", 0xF1},
    {"overview", 0xF2}, {"seaside", 0xF3}, {"fons", 0xF4}, {"dmorgue", 0xF5}, {"caelum", 0xF6}, {"cyber", 0xF8},
    {"boss1", 0xF9}, {"boss2", 0xFA}, {"dolor", 0xFB}, {"ravum", 0xFC}, {"sky", 0xFE}, {"morgue", 0xFF}};

const vector<string> npc_id_to_name = {
    "ninja", "rico", "sonic", "knuckles", "tails", "flowen", "elly", "momoka", "irene", "guild", "nurse"};

const unordered_map<string, uint8_t> name_to_npc_id = {
    {"ninja", 0}, {"rico", 1}, {"sonic", 2}, {"knuckles", 3}, {"tails", 4}, {"flowen", 5}, {"elly", 6}, {"momoka", 7},
    {"irene", 8}, {"guild", 9}, {"nurse", 10}};

bool npc_valid_for_version(uint8_t npc, Version version) {
  switch (version) {
    case Version::DC_NTE:
    case Version::DC_11_2000:
    case Version::DC_V1:
      return false;
    case Version::DC_V2:
    case Version::PC_NTE:
    case Version::PC_V2:
      return (npc < 5);
    case Version::GC_NTE:
    case Version::GC_V3:
    case Version::GC_EP3_NTE:
    case Version::GC_EP3:
    case Version::XB_V3:
      return (npc < 7);
    case Version::BB_V4:
      return (npc < 11);
    default:
      return false;
  }
}

const char* abbreviation_for_section_id(uint8_t section_id) {
  if (section_id < section_id_to_abbreviation.size()) {
    return section_id_to_abbreviation[section_id];
  } else {
    return "Unknown";
  }
}

const char* name_for_section_id(uint8_t section_id) {
  if (section_id < section_id_to_name.size()) {
    return section_id_to_name[section_id];
  } else {
    return "Unknown";
  }
}

uint8_t section_id_for_name(const string& name) {
  string lower_name = phosg::tolower(name);
  try {
    return name_to_section_id.at(lower_name);
  } catch (const out_of_range&) {
  }
  try {
    uint64_t x = stoul(name);
    if (x < section_id_to_name.size()) {
      return x;
    }
  } catch (const invalid_argument&) {
  } catch (const out_of_range&) {
  }
  return 0xFF;
}

const string& name_for_event(uint8_t event) {
  if (event < lobby_event_to_name.size()) {
    return lobby_event_to_name[event];
  } else {
    static const string ret = "Unknown lobby event";
    return ret;
  }
}

uint8_t event_for_name(const string& name) {
  try {
    return name_to_lobby_event.at(name);
  } catch (const out_of_range&) {
  }
  try {
    uint64_t x = stoul(name);
    if (x < lobby_event_to_name.size()) {
      return x;
    }
  } catch (const invalid_argument&) {
  } catch (const out_of_range&) {
  }
  return 0xFF;
}

const string& name_for_lobby_type(uint8_t type) {
  try {
    return lobby_type_to_name.at(type);
  } catch (const out_of_range&) {
    static const string ret = "Unknown lobby type";
    return ret;
  }
}

uint8_t lobby_type_for_name(const string& name) {
  try {
    return name_to_lobby_type.at(name);
  } catch (const out_of_range&) {
  }
  try {
    uint64_t x = stoul(name, nullptr, 0);
    if (lobby_type_to_name.count(x)) {
      return x;
    }
  } catch (const invalid_argument&) {
  } catch (const out_of_range&) {
  }
  return 0x80;
}

const string& name_for_npc(uint8_t npc) {
  try {
    return npc_id_to_name.at(npc);
  } catch (const out_of_range&) {
    static const string ret = "Unknown NPC";
    return ret;
  }
}

uint8_t npc_for_name(const string& name, Version version) {
  uint8_t npc_id = 0xFF;
  try {
    npc_id = name_to_npc_id.at(name);
  } catch (const out_of_range&) {
  }
  if (npc_id == 0xFF) {
    try {
      npc_id = stoul(name);
    } catch (const invalid_argument&) {
    } catch (const out_of_range&) {
    }
  }
  return npc_valid_for_version(npc_id, version) ? npc_id : 0xFF;
}

const char* name_for_char_class(uint8_t cls) {
  static const array<const char*, 12> names = {
      "HUmar", "HUnewearl", "HUcast", "RAmar", "RAcast", "RAcaseal", "FOmarl", "FOnewm", "FOnewearl", "HUcaseal",
      "FOmar", "RAmarl"};
  try {
    return names.at(cls);
  } catch (const out_of_range&) {
    return "Unknown";
  }
}

const char* abbreviation_for_char_class(uint8_t cls) {
  static const array<const char*, 12> names = {
      "HUmr", "HUnl", "HUct", "RAmr", "RAct", "RAcl", "FOml", "FOnm", "FOnl", "HUcl", "FOmr", "RAml"};
  try {
    return names.at(cls);
  } catch (const out_of_range&) {
    return "???";
  }
}

enum ClassFlag {
  MALE = 0x01,
  HUMAN = 0x02,
  NEWMAN = 0x04,
  ANDROID = 0x08,
  HUNTER = 0x10,
  RANGER = 0x20,
  FORCE = 0x40,
};

static array<uint8_t, 12> class_flags = {
    ClassFlag::HUNTER | ClassFlag::HUMAN | ClassFlag::MALE, // HUmar
    ClassFlag::HUNTER | ClassFlag::NEWMAN, // HUnewearl
    ClassFlag::HUNTER | ClassFlag::ANDROID | ClassFlag::MALE, // HUcast
    ClassFlag::RANGER | ClassFlag::HUMAN | ClassFlag::MALE, // RAmar
    ClassFlag::RANGER | ClassFlag::ANDROID | ClassFlag::MALE, // RAcast
    ClassFlag::RANGER | ClassFlag::ANDROID, // RAcaseal
    ClassFlag::FORCE | ClassFlag::HUMAN, // FOmarl
    ClassFlag::FORCE | ClassFlag::NEWMAN | ClassFlag::MALE, // FOnewm
    ClassFlag::FORCE | ClassFlag::NEWMAN, // FOnewearl
    ClassFlag::HUNTER | ClassFlag::ANDROID, // HUcaseal
    ClassFlag::FORCE | ClassFlag::HUMAN | ClassFlag::MALE, // FOmar
    ClassFlag::RANGER | ClassFlag::HUMAN, // RAmarl
};

bool char_class_is_male(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::MALE;
}

bool char_class_is_human(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::HUMAN;
}

bool char_class_is_newman(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::NEWMAN;
}

bool char_class_is_android(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::ANDROID;
}

bool char_class_is_hunter(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::HUNTER;
}

bool char_class_is_ranger(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::RANGER;
}

bool char_class_is_force(uint8_t cls) {
  return class_flags.at(cls) & ClassFlag::FORCE;
}

const char* name_for_difficulty(Difficulty difficulty) {
  static const array<const char*, 4> names = {"Normal", "Hard", "Very Hard", "Ultimate"};
  try {
    return names.at(static_cast<size_t>(difficulty));
  } catch (const out_of_range&) {
    return "Unknown";
  }
}

const char* token_name_for_difficulty(Difficulty difficulty) {
  static const array<const char*, 4> names = {"Normal", "Hard", "VeryHard", "Ultimate"};
  try {
    return names.at(static_cast<size_t>(difficulty));
  } catch (const out_of_range&) {
    return "Unknown";
  }
}

char abbreviation_for_difficulty(Difficulty difficulty) {
  static const array<char, 4> names = {'N', 'H', 'V', 'U'};
  try {
    return names.at(static_cast<size_t>(difficulty));
  } catch (const out_of_range&) {
    return '?';
  }
}

const char* name_for_language(Language language) {
  array<const char*, 8> names = {
      "Japanese", "English", "German", "French", "Spanish", "Simplified Chinese", "Traditional Chinese", "Korean"};
  size_t lang_index = static_cast<size_t>(language);
  return (lang_index < 8) ? names[lang_index] : "Unknown";
}

char char_for_language(Language language) {
  size_t lang_index = static_cast<size_t>(language);
  return (lang_index < 8) ? "JEGFSBTK"[lang_index] : '?';
}

Language language_for_char(char language_char) {
  switch (language_char) {
    case 'J':
    case 'j':
      return Language::JAPANESE;
    case 'E':
    case 'e':
      return Language::ENGLISH;
    case 'G':
    case 'g':
      return Language::GERMAN;
    case 'F':
    case 'f':
      return Language::FRENCH;
    case 'S':
    case 's':
      return Language::SPANISH;
    case 'B':
    case 'b':
      return Language::SIMPLIFIED_CHINESE;
    case 'T':
    case 't':
      return Language::TRADITIONAL_CHINESE;
    case 'K':
    case 'k':
      return Language::KOREAN;
    default:
      throw runtime_error("unknown language");
  }
}

Language language_for_name(const string& name) {
  if (name.size() == 1) {
    return language_for_char(name[0]);
  }
  string lower_name = phosg::tolower(name);
  if (lower_name == "japanese") {
    return Language::JAPANESE;
  }
  if (lower_name == "english") {
    return Language::ENGLISH;
  }
  if (lower_name == "german") {
    return Language::GERMAN;
  }
  if (lower_name == "french") {
    return Language::FRENCH;
  }
  if (lower_name == "spanish") {
    return Language::SPANISH;
  }
  if (lower_name == "simplified chinese") {
    return Language::SIMPLIFIED_CHINESE;
  }
  if (lower_name == "traditional chinese") {
    return Language::TRADITIONAL_CHINESE;
  }
  if (lower_name == "korean") {
    return Language::KOREAN;
  }
  throw runtime_error("unknown language");
}

const vector<string> tech_id_to_name = {
    "foie", "gifoie", "rafoie", "barta", "gibarta", "rabarta", "zonde", "gizonde", "razonde", "grants", "deband",
    "jellen", "zalure", "shifta", "ryuker", "resta", "anti", "reverser", "megid"};

const unordered_map<string, uint8_t> name_to_tech_id = {
    {"foie", 0}, {"gifoie", 1}, {"rafoie", 2}, {"barta", 3}, {"gibarta", 4}, {"rabarta", 5}, {"zonde", 6},
    {"gizonde", 7}, {"razonde", 8}, {"grants", 9}, {"deband", 10}, {"jellen", 11}, {"zalure", 12}, {"shifta", 13},
    {"ryuker", 14}, {"resta", 15}, {"anti", 16}, {"reverser", 17}, {"megid", 18}};

const string& name_for_technique(uint8_t tech) {
  try {
    return tech_id_to_name.at(tech);
  } catch (const out_of_range&) {
    static const string ret = "Unknown technique";
    return ret;
  }
}

uint8_t technique_for_name(const string& name) {
  try {
    return name_to_tech_id.at(name);
  } catch (const out_of_range&) {
  }
  try {
    uint64_t x = stoul(name);
    if (x < tech_id_to_name.size()) {
      return x;
    }
  } catch (const invalid_argument&) {
  } catch (const out_of_range&) {
  }
  return 0xFF;
}

const vector<const char*> name_for_mag_color = {
    "red", "blue", "yellow", "green", "purple", "black", "white", "cyan", "brown", "orange", "light-blue", "olive",
    "turquoise", "fuchsia", "grey", "cream", "pink", "dark-green", "costume"};

const unordered_map<string, uint8_t> mag_color_for_name = {
    {"red", 0x00}, {"blue", 0x01}, {"yellow", 0x02}, {"green", 0x03}, {"purple", 0x04}, {"black", 0x05},
    {"white", 0x06}, {"cyan", 0x07}, {"brown", 0x08}, {"orange", 0x09}, {"light-blue", 0x0A}, {"olive", 0x0B},
    {"turquoise", 0x0C}, {"fuchsia", 0x0D}, {"grey", 0x0E}, {"cream", 0x0F}, {"pink", 0x10}, {"dark-green", 0x11},
    {"costume-color", 0x12}};

static constexpr uint8_t F_CITY = FloorDefinition::Flag::CITY;
static constexpr uint8_t F_LOBBY = FloorDefinition::Flag::LOBBY;
static constexpr uint8_t F_BOSS = FloorDefinition::Flag::BOSS_ARENA;
static constexpr uint8_t F_V1 = FloorDefinition::Flag::EXISTS_ON_V1 | FloorDefinition::Flag::EXISTS_ON_V2 | FloorDefinition::Flag::EXISTS_ON_GC_NTE | FloorDefinition::Flag::EXISTS_ON_V3 | FloorDefinition::Flag::EXISTS_ON_V4;
static constexpr uint8_t F_V2 = FloorDefinition::Flag::EXISTS_ON_V2 | FloorDefinition::Flag::EXISTS_ON_GC_NTE | FloorDefinition::Flag::EXISTS_ON_V3 | FloorDefinition::Flag::EXISTS_ON_V4;
static constexpr uint8_t F_GCN = FloorDefinition::Flag::EXISTS_ON_GC_NTE | FloorDefinition::Flag::EXISTS_ON_V3 | FloorDefinition::Flag::EXISTS_ON_V4;
static constexpr uint8_t F_V3 = FloorDefinition::Flag::EXISTS_ON_V3 | FloorDefinition::Flag::EXISTS_ON_V4;
static constexpr uint8_t F_V4 = FloorDefinition::Flag::EXISTS_ON_V4;

static const std::vector<FloorDefinition> floor_defs{
    // clang-format off
    {Episode::EP1, 0x00, 0x00, 0x00, 0xFF, F_V1 | F_CITY,  "Pioneer2",            "P2",   "Pioneer 2", {"pioneer2", "p2", "city"}},
    {Episode::EP1, 0x01, 0x01, 0x01, 0x00, F_V1,           "Forest1",             "F1",   "Forest 1", {"forest1", "f1"}},
    {Episode::EP1, 0x02, 0x02, 0x02, 0x01, F_V1,           "Forest2",             "F2",   "Forest 2", {"forest2", "f2"}},
    {Episode::EP1, 0x03, 0x03, 0x03, 0x02, F_V1,           "Cave1",               "C1",   "Cave 1", {"caves1", "cave1", "c1"}},
    {Episode::EP1, 0x04, 0x04, 0x04, 0x03, F_V1,           "Cave2",               "C2",   "Cave 2", {"caves2", "cave2", "c2"}},
    {Episode::EP1, 0x05, 0x05, 0x05, 0x04, F_V1,           "Cave3",               "C3",   "Cave 3", {"caves3", "cave3", "c3"}},
    {Episode::EP1, 0x06, 0x06, 0x06, 0x05, F_V1,           "Mine1",               "M1",   "Mine 1", {"mines1", "mine1", "m1"}},
    {Episode::EP1, 0x07, 0x07, 0x07, 0x06, F_V1,           "Mine2",               "M2",   "Mine 2", {"mines2", "mine2", "m2"}},
    {Episode::EP1, 0x08, 0x08, 0x08, 0x07, F_V1,           "Ruins1",              "R1",   "Ruins 1", {"ruins1", "ruin1", "r1"}},
    {Episode::EP1, 0x09, 0x09, 0x09, 0x08, F_V1,           "Ruins2",              "R2",   "Ruins 2", {"ruins2", "ruin2", "r2"}},
    {Episode::EP1, 0x0A, 0x0A, 0x0A, 0x09, F_V1,           "Ruins3",              "R3",   "Ruins 3", {"ruins3", "ruin3", "r3"}},
    {Episode::EP1, 0x0B, 0x0B, 0x0B, 0x02, F_V1 | F_BOSS,  "Dragon",              "Dgn",  "Under the Dome (Dragon)", {"dragon"}},
    {Episode::EP1, 0x0C, 0x0C, 0x0C, 0x05, F_V1 | F_BOSS,  "DeRolLe",             "DRL",  "Underground Channel (De Rol Le)", {"derolle"}},
    {Episode::EP1, 0x0D, 0x0D, 0x0D, 0x07, F_V1 | F_BOSS,  "VolOpt",              "VO",   "Monitor Room (Vol Opt)", {"volopt"}},
    {Episode::EP1, 0x0E, 0x0E, 0x0E, 0x09, F_V1 | F_BOSS,  "DarkFalz",            "DF",   "???? (Dark Falz)", {"darkfalz"}},
    {Episode::EP1, 0x0F, 0x0F, 0x0F, 0xFF, F_V1 | F_LOBBY, "Lobby",               "Lby",  "Lobby", {"lobby"}},
    {Episode::EP1, 0x10, 0x10, 0x10, 0x09, F_V2,           "Battle1",             "B1",   "Spaceship", {"battle1"}},
    {Episode::EP1, 0x11, 0x11, 0x11, 0x09, F_V2,           "Battle2",             "B2",   "Palace", {"battle2"}},

    {Episode::EP2, 0x00, 0x00, 0x12, 0xFF, F_GCN | F_CITY, "Pioneer2",            "Lab",  "Lab", {"pioneer2", "p2", "lab", "labo"}},
    {Episode::EP2, 0x01, 0x13, 0x13, 0x00, F_GCN,          "VRTempleAlpha",       "VRTA", "VR Temple Alpha", {"vrtemplealpha", "templealpha", "vrta"}},
    {Episode::EP2, 0x02, 0x14, 0x14, 0x01, F_GCN,          "VRTempleBeta",        "VRTB", "VR Temple Beta", {"vrtemplebeta", "templebeta", "vrtb"}},
    {Episode::EP2, 0x03, 0x15, 0x15, 0x02, F_GCN,          "VRSpaceshipAlpha",    "VRSA", "VR Spaceship Alpha", {"vrspaceshipalpha", "spaceshipalpha", "vrsa"}},
    {Episode::EP2, 0x04, 0x16, 0x16, 0x03, F_GCN,          "VRSpaceshipBeta",     "VRSB", "VR Spaceship Beta", {"vrspaceshipbeta", "spaceshipbeta", "vrsb"}},
    {Episode::EP2, 0x05, 0x17, 0x17, 0x07, F_GCN,          "CentralControlArea",  "CCA",  "Central Control Area", {"centralcontrolarea", "cca"}},
    {Episode::EP2, 0x06, 0x18, 0x18, 0x04, F_GCN,          "JungleNorth",         "JN",   "Jungle Area North", {"junglenorth"}},
    {Episode::EP2, 0x07, 0x19, 0x19, 0x05, F_GCN,          "JungleEast",          "JE",   "Jungle Area East", {"jungleeast"}},
    {Episode::EP2, 0x08, 0x1A, 0x1A, 0x06, F_GCN,          "Mountain",            "Mtn",  "Mountain Area", {"mountain"}},
    {Episode::EP2, 0x09, 0x1B, 0x1B, 0x07, F_GCN,          "Seaside",             "SS",   "Seaside Area", {"seaside"}},
    {Episode::EP2, 0x0A, 0x1C, 0x1C, 0x08, F_GCN,          "SeabedUpper",         "SU",   "Seabed Upper Levels", {"seabedupper"}},
    {Episode::EP2, 0x0B, 0x1D, 0x1D, 0x09, F_GCN,          "SeabedLower",         "SL",   "Seabed Lower Levels", {"seabedlower"}},
    {Episode::EP2, 0x0C, 0x1E, 0x1E, 0x08, F_GCN | F_BOSS, "GalGryphon",          "GG",   "Cliffs of Gal Da Val (Gal Gryphon)", {"galgryphon"}},
    {Episode::EP2, 0x0D, 0x1F, 0x1F, 0x09, F_GCN | F_BOSS, "OlgaFlow",            "OF",   "Test Subject Disposal Area (Olga Flow)", {"olgaflow"}},
    {Episode::EP2, 0x0E, 0x20, 0x20, 0x02, F_GCN | F_BOSS, "BarbaRay",            "BR",   "VR Temple Final (Barba Ray)", {"barbaray"}},
    {Episode::EP2, 0x0F, 0x21, 0x21, 0x04, F_GCN | F_BOSS, "GolDragon",           "GD",   "VR Spaceship Final (Gol Dragon)", {"goldragon"}},
    {Episode::EP2, 0x10, 0xFF, 0x22, 0x07, F_V3,           "SeasideNight",        "SSN",  "Seaside Area (night)", {"seasidenight"}},
    {Episode::EP2, 0x11, 0xFF, 0x23, 0x09, F_V3,           "Tower",               "Twr",  "Control Tower", {"tower"}},

    {Episode::EP4, 0x00, 0xFF, 0x2D, 0xFF, F_V4 | F_CITY,  "Pioneer2",            "P2",   "Pioneer 2", {"pioneer2", "p2", "city"}},
    {Episode::EP4, 0x01, 0xFF, 0x24, 0x01, F_V4,           "CraterEast",          "CE",   "Crater (Eastern Route)", {"cratereast", "ce"}},
    {Episode::EP4, 0x02, 0xFF, 0x25, 0x02, F_V4,           "CraterWest",          "CW",   "Crater (Western Route)", {"craterwest", "cw"}},
    {Episode::EP4, 0x03, 0xFF, 0x26, 0x03, F_V4,           "CraterSouth",         "CS",   "Crater (Southern Route)", {"cratersouth", "cs"}},
    {Episode::EP4, 0x04, 0xFF, 0x27, 0x04, F_V4,           "CraterNorth",         "CN",   "Crater (Northern Route)", {"craternorth", "cn"}},
    {Episode::EP4, 0x05, 0xFF, 0x28, 0x05, F_V4,           "CraterInterior",      "CI",   "Crater Interior", {"craterinterior", "ci"}},
    {Episode::EP4, 0x06, 0xFF, 0x29, 0x06, F_V4,           "Desert1",             "D1",   "Subterranean Desert 1", {"desert1", "d1"}},
    {Episode::EP4, 0x07, 0xFF, 0x2A, 0x07, F_V4,           "Desert2",             "D2",   "Subterranean Desert 2", {"desert2", "d2"}},
    {Episode::EP4, 0x08, 0xFF, 0x2B, 0x08, F_V4,           "Desert3",             "D3",   "Subterranean Desert 3", {"desert3", "d3"}},
    {Episode::EP4, 0x09, 0xFF, 0x2C, 0x09, F_V4 | F_BOSS,  "SaintMilion",         "SM",   "Meteor Impact Site", {"saintmilion"}},
    {Episode::EP4, 0x0A, 0xFF, 0x2E, 0xFF, F_V4,           "TestMap",             "TM",   "Test Map", {"purgatory"}},
    // clang-format on
};

const FloorDefinition& FloorDefinition::get(Episode episode, uint8_t floor) {
  if (floor >= FloorDefinition::limit_for_episode(episode)) {
    throw runtime_error("invalid floor number");
  }
  switch (episode) {
    case Episode::EP1:
      return floor_defs.at(floor);
    case Episode::EP2:
      return floor_defs.at(floor + 0x12);
    case Episode::EP4:
      return floor_defs.at(floor + 0x24);
    default:
      throw logic_error("invalid episode");
  }
}

const FloorDefinition& FloorDefinition::get_by_drop_area_norm(Episode episode, uint8_t area_norm) {
  switch (episode) {
    case Episode::EP1:
      return floor_defs[area_norm + 1];
    case Episode::EP2:
      return floor_defs[0x13 + (area_norm >= 4) + area_norm];
    case Episode::EP4:
      return floor_defs[area_norm + 0x24];
    default:
      throw logic_error("invalid episode number");
  }
}

const FloorDefinition& FloorDefinition::get(Episode episode, const std::string& name) {
  static unordered_map<std::string, size_t> index_ep1;
  static unordered_map<std::string, size_t> index_ep2;
  static unordered_map<std::string, size_t> index_ep4;

  unordered_map<std::string, size_t>* index;
  switch (episode) {
    case Episode::EP1:
      index = &index_ep1;
      break;
    case Episode::EP2:
      index = &index_ep2;
      break;
    case Episode::EP4:
      index = &index_ep4;
      break;
    default:
      throw logic_error("invalid episode");
  }

  if (index->empty()) {
    for (size_t z = 0; z < floor_defs.size(); z++) {
      const auto& def = floor_defs[z];
      if (def.episode != episode) {
        continue;
      }
      for (const auto& alias : def.aliases) {
        index->emplace(alias, z);
      }
    }
  }

  return floor_defs[index->at(phosg::tolower(name))];
}

size_t FloorDefinition::limit_for_episode(Episode ep) {
  switch (ep) {
    case Episode::EP1:
    case Episode::EP2:
      return 0x12;
    case Episode::EP4:
      return 0x0B;
    default:
      return 0x00;
  }
}

uint32_t class_flags_for_class(uint8_t char_class) {
  static constexpr uint8_t flags[12] = {0x25, 0x2A, 0x31, 0x45, 0x51, 0x52, 0x86, 0x89, 0x8A, 0x32, 0x85, 0x46};
  if (char_class >= 12) {
    throw runtime_error("invalid character class");
  }
  return flags[char_class];
}

char char_for_challenge_rank(uint8_t rank) {
  if (rank > 2) {
    return '?';
  }
  return "BAS"[rank];
}

const array<size_t, 4> DEFAULT_MIN_LEVELS_V123({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP1({0, 19, 39, 79});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP2({0, 29, 49, 89});
const array<size_t, 4> DEFAULT_MIN_LEVELS_V4_EP4({0, 39, 79, 109});

const array<GameMode, 2> ALL_GAME_MODES_V1 = {GameMode::NORMAL, GameMode::BATTLE};
const array<GameMode, 3> ALL_GAME_MODES_V23 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE};
const array<GameMode, 4> ALL_GAME_MODES_V4 = {GameMode::NORMAL, GameMode::BATTLE, GameMode::CHALLENGE, GameMode::SOLO};
const array<Episode, 1> ALL_EPISODES_V12 = {Episode::EP1};
const array<Episode, 2> ALL_EPISODES_V3 = {Episode::EP1, Episode::EP2};
const array<Episode, 3> ALL_EPISODES_V4 = {Episode::EP1, Episode::EP2, Episode::EP4};
const array<Difficulty, 3> ALL_DIFFICULTIES_V1 = {Difficulty::NORMAL, Difficulty::HARD, Difficulty::VERY_HARD};
const array<Difficulty, 4> ALL_DIFFICULTIES_V234 = {Difficulty::NORMAL, Difficulty::HARD, Difficulty::VERY_HARD, Difficulty::ULTIMATE};
