#ifndef _H_XMLLOADER_H
#define _H_XMLLOADER_H

#include <functional>
#include <glm/glm.hpp>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

#include <filesystem>
#include <fstream>
#include <sstream>
#include <tinyxml2.h>

struct Tile;

// Except for the player, these definitions are mostly overrides of the monster
// data
struct EntityData {
  bool isPlayer;
  bool animated;
  int x = 0, y = 0;
  float hp;
  std::string monsterDef;
  std::string graphic;
  std::string weapon = "gun/pistol";
  std::string script;
};

struct MapData {
  std::string name;
  struct TileSet {
    int startID = 1;
    std::string path;
  };
  std::vector<TileSet> tileSets;
  int width = 0, height = 0;
  float tileSize = 32.f;
  // 3D array, 0: layer, 1: y, 2: x
  // Holding tile startID + ids, 0 is an empty tile
  std::vector<std::vector<std::vector<int>>> tiles;
};

struct TileSetData {
  std::string name;
  std::string type;
  std::string file;
  int width = 0, height = 0;
  int columns = 0;
  int tileCount = 0;
  float tileSize = 64.f;
  struct TileData {
    int id = 0;
    std::string type;
    bool walkable = true;
    struct ObjectData {
      int id = 0;
      std::string name;
      glm::vec2 pos;
      glm::vec2 size;
      bool collidable = false;
      bool pickup = false;
    };
    std::vector<std::shared_ptr<ObjectData>> objects;
  };
  std::vector<std::shared_ptr<TileData>> tiles;
};

struct SceneData {
  std::string id;
  std::string type;
  std::string bgFile;

  const MapData *map;
  std::vector<EntityData> entities;
};

// Store our script as a string so we only need to read the file at boot
struct ScriptData {
  std::string id;
  std::string fileName;
  std::string script;
};

struct WeaponData {
  std::string id;
  float fireSpeed = 250.0f;
  int clipSize = 21;
  int maxAmmo = 512;
  std::string script = "";
  std::string graphic;
  bool animated = false;
  float sizeX = 50.f, sizeY = 50.f;
  float offsetX = 0.f, offsetY = 0.f;
  float bulletSizeX = 50.f, bulletSizeY = 50.f;
  std::string bulletGraphic;
  bool bulletAnimated = false;
  float bulletSpread = 1.0f, bulletSpeed = 3.0f, bulletDrop = 500.f;
  float modMin = 0.5f, modMax = 1.0f;
};

// ID is the new tactic I've decided on.
// Each animationable object will be given a prefix
// denoting their type and object so <type>/<object>
// This prefix will be used to look up the animation on
// the animation map
struct AnimationData {
  std::string id;
  std::string spriteAtlas;
  bool directional = false;
  bool oneShot;
  float FPS = 1.f;
  float frameSize;
};

// This holds a lot of defaults, but the scene can
// override a large portion of the definitions here
struct MonsterData {
  std::string id;
  std::string anim;
  std::string weapon;
  bool aggressive = false;
  std::string behaviour;
  float hp;
};

struct SoundData {
  std::string id; // <type>/<object>/<state>
  std::string path;
  bool spatial;
};

class XMLLoader {
public:
  XMLLoader() {}
  bool loadScenes(const char *sceneFolder);
  bool loadWeapons(const char *weaponFolder);
  bool loadAnimations(const char *animationFolder);
  bool loadTileSets(const char *tileSetFolder);
  bool loadMaps(const char *mapFolder);
  bool loadMonsters(const char *monsterFolder);
  // luaLoader is the function we use to load the lua file.
  // This will be defined in our resource manager
  bool loadScripts(const char *scriptFolder,
                   std::function<bool(ScriptData *)> luaLoader);
  bool loadSoundEffects(const char *soundFolder);

  const SceneData *getSceneData(const std::string &id) const {
    try {
      return scenes.at(id).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const ScriptData *getScriptData(const std::string &id) const {
    try {
      return scripts.at(id).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const MapData *getMapData(const std::string &name) const {
    try {
      return maps.at(name).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const WeaponData *getWeaponData(const std::string &id) const {
    try {
      return weapons.at(id).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const AnimationData *getAnimationData(const std::string &id) const {
    try {
      return animations.at(id).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const TileSetData *getTileSetData(const std::string &name) const {
    try {
      return tileSets.at(name).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  const SoundData *getSoundData(const std::string &id) const {
    try {
      return sounds.at(id).get();
    } catch (std::exception &) {
      return nullptr;
    }
  }

  // return a full set of animations, may need further optimization.
  // one idea is when loading animations we create a seperate map that holds
  // each set by their reference, so we can just do a simple, hash table lookup.
  std::vector<AnimationData *>
  getAnimationSet(const std::string &prefix) const {
    std::vector<AnimationData *> animSet;
    animSet.reserve(animations.size());
    for (const auto &[id, anim] : animations) {
      if (id.starts_with(prefix))
        animSet.push_back(anim.get());
    }
    animSet.shrink_to_fit();
    return animSet;
  }

  void clearData() {
    scenes.clear();
    weapons.clear();
    animations.clear();
    maps.clear();
    tileSets.clear();
  }

protected:
  bool loadXmlScene(const char *xmlFile, SceneData *out);
  bool loadEntityData(const char *xmlFile, SceneData *out);
  bool loadTile(tinyxml2::XMLElement *tileElement, TileSetData::TileData *out);
  bool loadObject(tinyxml2::XMLElement *objectElement,
                  TileSetData::TileData::ObjectData *out);

private:
  std::unordered_map<std::string, std::unique_ptr<SceneData>> scenes;
  std::unordered_map<std::string, std::unique_ptr<ScriptData>> scripts;
  std::unordered_map<std::string, std::unique_ptr<WeaponData>> weapons;
  std::unordered_map<std::string, std::unique_ptr<AnimationData>> animations;
  std::unordered_map<std::string, std::unique_ptr<MonsterData>> monsters;
  std::unordered_map<std::string, std::unique_ptr<MapData>> maps;
  std::unordered_map<std::string, std::unique_ptr<TileSetData>> tileSets;
  std::unordered_map<std::string, std::unique_ptr<SoundData>> sounds;
};

#endif // _H_XMLLOADER_H
