#include "utility/xmlloader.h"

#include "utility/logger.h"
#include <filesystem>
#include <tinyxml2.h>

/*
        Loading every scene in the scene folder and storing in hashmap scenes
        hashkey is the id of the scene
*/
bool XMLLoader::loadScenes(const char *sceneFolder) {
  std::filesystem::path folder(sceneFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder not found!", sceneFolder);
  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;
    SceneData scene;
    if (!loadXmlScene((const char *)file.path().string().c_str(), &scene))
      continue;
    scenes.try_emplace(scene.id, std::make_unique<SceneData>(scene));
  }
  return true;
}

/*
        Loading scene data
*/
bool XMLLoader::loadXmlScene(const char *xmlFile, SceneData *out) {
  tinyxml2::XMLDocument doc;
  if (doc.LoadFile(xmlFile) != tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to load file: {}", xmlFile);
  tinyxml2::XMLElement *scene = doc.FirstChildElement("scene");

  const char *type, *id, *mapName, *bgFile;
  if (scene->QueryStringAttribute("type", &type) != tinyxml2::XML_SUCCESS ||
      scene->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS)
    ERROR_LOG("Could not read type or id tag in file: {}", xmlFile);
  out->type = type;
  out->id = id;

  if (scene->QueryStringAttribute("bg", &bgFile) != tinyxml2::XML_SUCCESS)
    LOG(WARN, "No background set! attribute 'bg' is empty", NULL);
  else
    out->bgFile = bgFile;

  tinyxml2::XMLElement *map = scene->FirstChildElement("map");
  if (map == NULL)
    ERROR_LOG("Could not find 'map' tag in file: {}", xmlFile);
  if (map->QueryStringAttribute("name", &mapName) != tinyxml2::XML_SUCCESS)
    ERROR_LOG("Could not find attribute 'name' in map tag. file: {}", xmlFile)

  if (!(out->map = getMapData(mapName)))
    return false;
  if (!loadEntityData(xmlFile, out))
    return false;
  LOG(DEBUG, "Loading scene: {}", xmlFile);
  return true;
}

/*
 * Load monster definitions
 */
bool XMLLoader::loadMonsters(const char *monsterFolder) {
  std::filesystem::path folder(monsterFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("''{}' folder does not exist!", monsterFolder);

  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;
    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *e = doc.FirstChildElement("monster");
         e != nullptr; e = e->NextSiblingElement("monster")) {
      const char *id, *anim, *weapon, *behaviour;

      MonsterData data;
      data.aggressive = false;

      // The rules for creating a monster is pretty strict, since we can always
      // just create entities without creating a definition for them as well.
      if (e->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS ||
          e->QueryStringAttribute("anim", &anim) != tinyxml2::XML_SUCCESS ||
          e->QueryStringAttribute("weapon", &weapon) != tinyxml2::XML_SUCCESS ||
          e->QueryStringAttribute("behaviour", &behaviour) !=
              tinyxml2::XML_SUCCESS ||
          e->QueryFloatAttribute("hp", &data.hp) != tinyxml2::XML_SUCCESS)
        continue;

      e->QueryBoolAttribute("aggressive", &data.aggressive);

      data.id = id;
      data.anim = anim;
      data.weapon = weapon;
      data.behaviour = behaviour;

      monsters.try_emplace(data.id, std::make_unique<MonsterData>(data));
    }
  }
  return true;
}
/*
 * Loading scripts from the xml file then storing the information inside of the
 * ScriptData definition struct. This includes storing our script contents. This
 * is mostly a test, but I feel like it would be faster to hold the scripts as
 * raw string we store at boot then load based on their ID. It certainly makes
 * creating definitions easier and cleaner to look at.
 *
 * It is the responsibilty of the resource manager to open read read the lua
 * file via the luaLoader function!
 *
 */
bool XMLLoader::loadScripts(const char *scriptFolder,
                            std::function<bool(ScriptData *)> luaLoader) {
  std::filesystem::path folder(scriptFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder does not exist!", scriptFolder);

  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;

    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *e = doc.FirstChildElement("script");
         e != nullptr; e = e->NextSiblingElement("script")) {
      ScriptData scriptData;
      const char *id, *fileName;
      if (e->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS ||
          e->QueryStringAttribute("file", &fileName) != tinyxml2::XML_SUCCESS)
        continue;

      scriptData.id = id;
      scriptData.fileName = fileName;

      if (!luaLoader(&scriptData)) {
        LOG(ERROR, "Failed to load script '{}'", fileName);
        continue;
      }

      scripts.try_emplace(scriptData.id,
                          std::make_unique<ScriptData>(scriptData));
    }
  }
  return true;
}
/*
        Load entity data held in the scene file, store inside of the SceneData
   out parameter
*/
bool XMLLoader::loadEntityData(const char *xmlFile, SceneData *out) {
  tinyxml2::XMLDocument doc;
  if (doc.LoadFile(xmlFile) != tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to load file: {}", xmlFile);

  tinyxml2::XMLElement *entities =
      doc.FirstChildElement()->FirstChildElement("entities");

  // Adding the player. Player must be in the scene or the scene will not load!
  tinyxml2::XMLElement *playerElement = entities->FirstChildElement("player");
  if (playerElement == NULL)
    ERROR_LOG("Player element not found in scene! File: {}", xmlFile);

  EntityData playData;
  const char *graphic, *weaponName = "pistol";

  if (playerElement->QueryIntAttribute("x", &playData.x) !=
          tinyxml2::XML_SUCCESS ||
      playerElement->QueryIntAttribute("y", &playData.y) !=
          tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to find x or y attribute in 'player' tag. File: {}",
              xmlFile);
  playerElement->QueryStringAttribute("weapon", &weaponName);
  tinyxml2::XMLElement *anim = playerElement->FirstChildElement("animation");
  if (anim == NULL) {
    playData.animated = false;
    tinyxml2::XMLElement *sprite = playerElement->FirstChildElement("sprite");
    if (sprite == NULL)
      ERROR_LOG("Could not find tag 'sprite' in 'player' tag. File: {}",
                xmlFile);
    if (sprite->QueryStringAttribute("file", &graphic) != tinyxml2::XML_SUCCESS)
      ERROR_LOG("Could not find attribute 'file' in 'player' -> 'sprite' tag. "
                "File: {}",
                xmlFile);
  } else {
    playData.animated = true;
    if (anim->QueryStringAttribute("id", &graphic) != tinyxml2::XML_SUCCESS)
      ERROR_LOG("Could not find 'name' attribute in 'animation' tag. File: {}",
                xmlFile);
  }
  playData.isPlayer = true;
  playData.graphic = graphic;
  playData.weapon = weaponName;

  out->entities.push_back(playData);
  LOG(DEBUG, "Loaded player with sprite: {} in file: {}", graphic, xmlFile);

  // Adding every other entity...
  // TODO: Add npcs to game and enable their use with XMLLoader

  for (tinyxml2::XMLElement *e = entities->FirstChildElement("entity");
       e != NULL; e = e->NextSiblingElement("entity")) {
    EntityData data;
    const char *monsterDef, *graphic, *weaponID, *scriptID;
    if (e->QueryIntAttribute("x", &data.x) != tinyxml2::XML_SUCCESS ||
        e->QueryIntAttribute("y", &data.y) != tinyxml2::XML_SUCCESS)
      ERROR_LOG("Could not load position coordinates for entity. File: {}",
                xmlFile);
    e->QueryStringAttribute("weapon", &weaponName);
    // If we find that the entity has a monster definition we will fill in the
    // defaults Having a monster definition means we must be animated by default
    // and we will prefer the animated default for now.
    if (e->QueryStringAttribute("monster_id", &monsterDef) == tinyxml2::XML_SUCCESS) {
			if (monsters[monsterDef]) {
				// Setting up the defaults
				data.animated = true;
				data.monsterDef = monsterDef;
				weaponID = monsters[monsterDef]->weapon.c_str();
				graphic = monsters[monsterDef]->anim.c_str();
				scriptID = monsters[monsterDef]->behaviour.c_str();
			}
    }
    try {
      // So if we look for the animation tag and there is no monster definition
      // only then will we take in a sprite as the graphic
      tinyxml2::XMLElement *anim = e->FirstChildElement("animation");
      if (anim == NULL && data.monsterDef.empty()) {
        data.animated = false;
        tinyxml2::XMLElement *sprite = e->FirstChildElement("sprite");
        if (sprite == NULL)
          continue;
        if (sprite->QueryStringAttribute("file", &graphic) !=
            tinyxml2::XML_SUCCESS)
          continue;
      } else if (data.monsterDef.empty()) {
        data.animated = true;
        if (anim->QueryStringAttribute("id", &graphic) != tinyxml2::XML_SUCCESS)
          continue;
      } else if (anim != NULL) {
        // We will take alternative animation ids if specified
        data.animated = true;
        anim->QueryStringAttribute("id", &graphic);
      }
    } catch (std::exception &e) {
      LOG(ERROR, "Failed to load entity! ''{}'", e.what());
      continue;
    }
    // We just overload the script id if there is an overload given
    tinyxml2::XMLElement *script = e->FirstChildElement("script");
    if (script != NULL)
      script->QueryStringAttribute("id", &scriptID);
    data.isPlayer = false;
    data.graphic = graphic;
    data.script = scriptID;
    data.weapon = weaponName;

    out->entities.push_back(data);
  }

  return true;
}

float getFloatIfExists(tinyxml2::XMLElement *e) {
  float buf = 0.f;
  if (e) {
    e->QueryFloatText(&buf);
  }
  return buf;
}

/*
   Load every weapon file, weapon nodes can be fit together in one file or
   placed in seperate files. hash key is the weapon id
*/
bool XMLLoader::loadWeapons(const char *weaponFolder) {
  // We are gonna check every xml file within the weaponFolder, then check every
  // weapon node within each file.
  std::filesystem::path folder(weaponFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder does not exist!", weaponFolder);
  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;
    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;
    tinyxml2::XMLElement *guns = doc.FirstChildElement("weapons");
    if (guns == NULL)
      continue;

    for (tinyxml2::XMLElement *weapon = guns->FirstChildElement("weapon");
         weapon != NULL; weapon = weapon->NextSiblingElement("weapon")) {
      // populate weapon data into this temp buffer, then push it into our list
      // of weapons
      WeaponData data;
      const char *id, *graphic, *bulletGraphic, *script;
      // Getting top level weapon data, attribs in the weapon Node
      if (weapon->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS)
        continue;
      weapon->QueryFloatAttribute("fireSpeed", &data.fireSpeed);
      weapon->QueryIntAttribute("maxAmmo", &data.maxAmmo);
      weapon->QueryIntAttribute("clipSize", &data.clipSize);
      // Getting script file if Node exists
      tinyxml2::XMLElement *wepScript = weapon->FirstChildElement("script");
      if (wepScript)
        wepScript->QueryStringAttribute("id", &script);
      else
        script = "";
      // Getting weapon sprite information, held in the sprite node
      tinyxml2::XMLElement *anim = weapon->FirstChildElement("animation");
      if (anim) {
        data.animated = true;
        if (anim->ChildElementCount() != 0) {
          tinyxml2::XMLElement *size = anim->FirstChildElement("size");
          size->QueryFloatAttribute("x", &data.sizeX);
          size->QueryFloatAttribute("y", &data.sizeY);
          tinyxml2::XMLElement *offset = anim->FirstChildElement("offset");
          offset->QueryFloatAttribute("x", &data.offsetX);
          offset->QueryFloatAttribute("y", &data.offsetY);
        }
      } else {
        tinyxml2::XMLElement *wepSprite = weapon->FirstChildElement("sprite");
        if (wepSprite == NULL)
          continue;

        wepSprite->QueryStringAttribute("file", &graphic);
        data.animated = false;
        if (wepSprite->ChildElementCount() != 0) {
          tinyxml2::XMLElement *size = wepSprite->FirstChildElement("size");
          size->QueryFloatAttribute("x", &data.sizeX);
          size->QueryFloatAttribute("y", &data.sizeY);
          tinyxml2::XMLElement *offset = wepSprite->FirstChildElement("offset");
          offset->QueryFloatAttribute("x", &data.offsetX);
          offset->QueryFloatAttribute("y", &data.offsetY);
        }
      }
      // getting bullet information held in the bullet node and each child node
      // held therein
      tinyxml2::XMLElement *bullet = weapon->FirstChildElement("bullet");

      if (bullet->FindAttribute("anim")) {
        data.bulletAnimated = true;
        if (bullet->QueryStringAttribute("anim", &bulletGraphic) !=
            tinyxml2::XML_SUCCESS)
          continue;
      } else {
        data.bulletAnimated = false;
        if (bullet->QueryStringAttribute("sprite", &bulletGraphic) !=
            tinyxml2::XML_SUCCESS)
          continue;
      }

      if (bullet->FirstChildElement("size")->QueryFloatAttribute(
              "x", &data.bulletSizeX) != tinyxml2::XML_SUCCESS ||
          bullet->FirstChildElement("size")->QueryFloatAttribute(
              "y", &data.bulletSizeY) != tinyxml2::XML_SUCCESS)
        continue;

      data.id = id;
      data.script = script;
      data.bulletGraphic = bulletGraphic;

      tinyxml2::XMLElement *spread = bullet->FirstChildElement("spread");
      tinyxml2::XMLElement *speed = bullet->FirstChildElement("speed");
      tinyxml2::XMLElement *drop = bullet->FirstChildElement("drop");
      tinyxml2::XMLElement *modifier = bullet->FirstChildElement("modifier");
      if (spread)
        spread->QueryFloatText(&data.bulletSpread);
      if (speed)
        speed->QueryFloatText(&data.bulletSpeed);
      if (drop)
        drop->QueryFloatText(&data.bulletDrop);
      if (modifier) {
        modifier->QueryFloatAttribute("min", &data.modMin);
        modifier->QueryFloatAttribute("max", &data.modMax);
      }

      LOG(DEBUG, "Loaded {} from {}", data.id,
          file.path().filename().generic_string());
      weapons.try_emplace(data.id, std::make_unique<WeaponData>(data));
    }
  }
  return (!weapons.empty());
}

/*
 * Load each animation node and store inside a hashmap by their id. Each object
 * that needs an animation will be given a prefix <type>/<object>. Which will be
 * used to look up the corresponding animation
 */
bool XMLLoader::loadAnimations(const char *animationFolder) {
  std::filesystem::path folder(animationFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder does not exist!", animationFolder);
  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;

    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *e = doc.FirstChildElement("animation");
         e != NULL; e = e->NextSiblingElement("animation")) {
      AnimationData animData;
      const char *id, *spriteAtlas;

      if (e->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS)
        continue;

      if (e->QueryStringAttribute("atlas", &spriteAtlas) !=
              tinyxml2::XML_SUCCESS ||
          e->QueryFloatAttribute("frameSize", &animData.frameSize) !=
              tinyxml2::XML_SUCCESS)
        continue;

      e->QueryFloatAttribute("FPS", &animData.FPS);
      e->QueryBoolAttribute("directional", &animData.directional);

      animData.id = id;
      animData.spriteAtlas = spriteAtlas;
      animations.try_emplace(animData.id,
                             std::make_unique<AnimationData>(animData));
    }
  }
  return true;
}

/*
        Start of (LoadTileSets)

        Load every tileset and store them in hashmap tileSets
        hashkey is the <parent directory>/<filename>, this is due to the
   expected file structure, being Resources
        |
          - maps
            |
             - tilesets
        Thats is, the tilesets folder is within the maps folder.
        This makes it easier to import maps from the Tiled program.
        Refer to the Tiled project file for the file structure
*/
bool XMLLoader::loadTileSets(const char *tileSetFolder) {
  std::filesystem::path folder(tileSetFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder failed to load!", tileSetFolder);

  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;
    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;

    tinyxml2::XMLElement *tileSet = doc.FirstChildElement("tileset");
    if (tileSet == NULL)
      continue;
    TileSetData tileSetData;
    const char *setName, *setType = "shooter", *setFile;

    tileSet->QueryStringAttribute("class", &setType);

    // Read attributes of tileset element
    if (tileSet->QueryStringAttribute("name", &setName) !=
            tinyxml2::XML_SUCCESS ||
        tileSet->QueryFloatAttribute("tilewidth", &tileSetData.tileSize) !=
            tinyxml2::XML_SUCCESS ||
        tileSet->QueryIntAttribute("tilecount", &tileSetData.tileCount) !=
            tinyxml2::XML_SUCCESS ||
        tileSet->QueryIntAttribute("columns", &tileSetData.columns) !=
            tinyxml2::XML_SUCCESS)
      continue;

    tinyxml2::XMLElement *image = tileSet->FirstChildElement("image");
    if (image == NULL)
      continue;

    // Reading image element attribs
    if (image->QueryStringAttribute("source", &setFile) !=
            tinyxml2::XML_SUCCESS ||
        image->QueryIntAttribute("width", &tileSetData.width) !=
            tinyxml2::XML_SUCCESS ||
        image->QueryIntAttribute("height", &tileSetData.height) !=
            tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *tileElement = tileSet->FirstChildElement("tile");
         tileElement != NULL;
         tileElement = tileElement->NextSiblingElement("tile")) {
      TileSetData::TileData tileData;
      if (!loadTile(tileElement, &tileData))
        continue;
      tileSetData.tiles.push_back(
          std::make_shared<TileSetData::TileData>(tileData));
    }
    tileSetData.name = setName;
    tileSetData.type = setType;
    tileSetData.file =
        file.path().parent_path().string() + "/" + std::string(setFile);
    std::string key =
        folder.filename().string() + "/" + file.path().filename().string();

    tileSets.try_emplace(key, std::make_unique<TileSetData>(tileSetData));
  }
  return true;
}

bool XMLLoader::loadTile(tinyxml2::XMLElement *tileElement,
                         TileSetData::TileData *out) {
  TileSetData::TileData tileData;
  const char *tileType;

  if (tileElement == NULL)
    ERROR_LOG("Failed to find 'tile' tag.", NULL);
  if (tileElement->QueryIntAttribute("id", &tileData.id) !=
          tinyxml2::XML_SUCCESS ||
      tileElement->QueryStringAttribute("type", &tileType) !=
          tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to load tile id or type. {}", tileElement->Value());

  if (std::string(tileType).compare("object") == 0) {
    /*
            Refer to .tsx file of tile set for specific details regarding the
       layout of the tile element. You will notice each tile has the potential
       to be a container for objects, so that is how we are handling it here. If
       the tileType was specified as an object, but it contains no objects, then
       we don't load the tile. Objects are defined in the Tiled program using
       the collision editor.
    */
    tinyxml2::XMLElement *objectGroup =
        tileElement->FirstChildElement("objectgroup");
    if (objectGroup == NULL)
      ERROR_LOG("Failed to find tag 'objectgroup' in tile id: {}", tileData.id);
    for (tinyxml2::XMLElement *obj = objectGroup->FirstChildElement("object");
         obj != NULL; obj = obj->NextSiblingElement("object")) {
      TileSetData::TileData::ObjectData objData;
      if (!loadObject(obj, &objData))
        continue;
      tileData.objects.push_back(
          std::make_shared<TileSetData::TileData::ObjectData>(objData));
    }
    if (tileData.objects.empty())
      ERROR_LOG("No objects found", NULL);
  } else {
    /*
            May support multiple properties in the future with a future property
       struct to hold any value type But this may not be needed, so we'll just
       capture the walkable property for now. Assume default of there is no
       properties tag or walkable property, just use nested if's
    */
    tinyxml2::XMLElement *properties =
        tileElement->FirstChildElement("properties");
    if (properties == NULL) {
      LOG(WARN, "No properties found on tile id: {} assuming defaults",
          tileData.id);
    } else {
      tinyxml2::XMLElement *propWalk =
          properties->FirstChildElement("property");
      if (propWalk == NULL) {
        LOG(WARN, "No walkable propery found on tile id: {} assuming walkable",
            tileData.id);
      } else {
        for (auto &prop = propWalk; prop != NULL;
             prop = prop->NextSiblingElement()) {
          if (prop->Attribute("name", "walkable")) {
            prop->QueryBoolAttribute("value", &tileData.walkable);
            break;
          }
        }
      }
    }
  }

  tileData.type = tileType;

  *out = tileData;
  return true;
}

bool XMLLoader::loadObject(tinyxml2::XMLElement *objElement,
                           TileSetData::TileData::ObjectData *out) {
  TileSetData::TileData::ObjectData objData;
  const char *objName;

  // avoid null pointer exception
  if (objElement == NULL)
    ERROR_LOG("Failed to find 'object' tag", NULL);
  // load id and name
  if (objElement->QueryIntAttribute("id", &objData.id) !=
          tinyxml2::XML_SUCCESS ||
      objElement->QueryStringAttribute("name", &objName) !=
          tinyxml2::XML_SUCCESS)
    ERROR_LOG("No id or name found for object. {}", objElement->Value());
  // load position into vec2
  if (objElement->QueryFloatAttribute("x", &objData.pos.x) !=
          tinyxml2::XML_SUCCESS ||
      objElement->QueryFloatAttribute("y", &objData.pos.y) !=
          tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to load position coordinates for object: {}",
              objData.name);
  // load size into seperate vec2
  if (objElement->QueryFloatAttribute("width", &objData.size.x) !=
          tinyxml2::XML_SUCCESS ||
      objElement->QueryFloatAttribute("height", &objData.size.y) !=
          tinyxml2::XML_SUCCESS)
    ERROR_LOG("Failed to load size of object: {}", objData.name);

  // refer to comment in XMLLoader::loadTile regarding the properties portion as
  // to why we return true here
  tinyxml2::XMLElement *properties =
      objElement->FirstChildElement("properties");
  if (properties != NULL) {
    tinyxml2::XMLElement *propCollide =
        properties->FirstChildElement("property");
    if (propCollide != NULL && propCollide->Attribute("name", "collidable"))
      propCollide->QueryBoolAttribute("value", &objData.collidable);
  }

  objData.name = objName;

  *out = objData;
  return true;
}
/* End of (LoadTileSets) */

bool XMLLoader::loadMaps(const char *mapFolder) {
  std::filesystem::path folder(mapFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("'{}' folder not found!", mapFolder);

  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;
    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().generic_string().c_str()) !=
        tinyxml2::XML_SUCCESS)
      continue;

    tinyxml2::XMLElement *map = doc.FirstChildElement("map");
    if (map == NULL)
      continue;
    MapData mapData;

    if (map->QueryIntAttribute("width", &mapData.width) !=
            tinyxml2::XML_SUCCESS ||
        map->QueryIntAttribute("height", &mapData.height) !=
            tinyxml2::XML_SUCCESS ||
        map->QueryFloatAttribute("tilewidth", &mapData.tileSize) !=
            tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *tileSet = map->FirstChildElement("tileset");
         tileSet != NULL; tileSet = tileSet->NextSiblingElement("tileset")) {
      int firstGID = 0;
      const char *tileSetPath;
      if (tileSet->QueryStringAttribute("source", &tileSetPath) !=
              tinyxml2::XML_SUCCESS ||
          tileSet->QueryIntAttribute("firstgid", &firstGID) !=
              tinyxml2::XML_SUCCESS)
        continue;
      mapData.tileSets.push_back({firstGID, tileSetPath});
    }

    if (mapData.tileSets.empty())
      continue;

    mapData.tiles.reserve(10);
    mapData.tiles.resize(10);

    for (int layer = 0; layer < 10; ++layer) {
      mapData.tiles[layer].reserve(mapData.height);
      mapData.tiles[layer].resize(mapData.height);
      for (int y = 0; y < mapData.height; ++y) {
        mapData.tiles[layer][y].reserve(mapData.width);
        mapData.tiles[layer][y].resize(mapData.width);
      }
    }

    int layerNumber = 0;
    for (tinyxml2::XMLElement *layer = map->FirstChildElement("layer");
         layer != NULL; layer = layer->NextSiblingElement("layer")) {
      tinyxml2::XMLElement *data = layer->FirstChildElement("data");
      if (data == NULL)
        continue;
      if (layerNumber >= 10)
        continue;

      std::string idSet = data->GetText();
      std::string tileRow;
      std::string tileString;

      std::stringstream ssidSet(idSet);
      int x = 0, y = -1;

      while (std::getline(ssidSet, tileRow) && y < mapData.height) {
        std::stringstream ssid(tileRow);
        x = 0;
        while (std::getline(ssid, tileString, ',') && x < mapData.width) {
          int id = std::stoi(tileString);
          mapData.tiles[layerNumber][y][x] = id;
          x++;
        }
        y++;
      }

      layerNumber += 1;
    }

    mapData.tiles.resize(layerNumber);
    mapData.tiles.shrink_to_fit();
    /* TODO: Add object layer */

    mapData.name = file.path().stem().string();

    maps.try_emplace(mapData.name, std::make_unique<MapData>(mapData));
  }
  return true;
}

bool XMLLoader::loadSoundEffects(const char *soundFolder) {
  std::filesystem::path folder(soundFolder);
  if (!std::filesystem::exists(folder) ||
      !std::filesystem::is_directory(folder))
    ERROR_LOG("Directory '{}' does not exist!", soundFolder);

  for (auto &file : std::filesystem::directory_iterator(folder)) {
    if (!file.path().has_extension() || !file.path().has_filename() ||
        !file.exists() || file.is_directory())
      continue;

    tinyxml2::XMLDocument doc;
    if (doc.LoadFile(file.path().string().c_str()) != tinyxml2::XML_SUCCESS)
      continue;

    for (tinyxml2::XMLElement *e = doc.FirstChildElement("sound"); e != NULL;
         e = e->NextSiblingElement("sound")) {
      SoundData sound;
      const char *id, *path;
      if (e->QueryStringAttribute("id", &id) != tinyxml2::XML_SUCCESS ||
          e->QueryStringAttribute("path", &path) != tinyxml2::XML_SUCCESS)
        continue;
      e->QueryBoolAttribute("spatial", &sound.spatial);

      sound.id = id;
      sound.path = path;

      sounds.try_emplace(sound.id, std::make_unique<SoundData>(sound));
    }
  }
  return true;
}
