#include "gameplay/map.h"
#include "graphics/texture.h"
#include "utility/logger.h"
#include "utility/resourcemanager.h"
#include "utility/xmlloader.h"

#include <glm/gtc/matrix_transform.hpp>

Map::Map(const MapData *mapData, const unsigned shaderID,
         std::shared_ptr<ResourceManager> resourceManager)
    : mapData(mapData), tileIds(mapData->tiles) {
  this->shaderID = shaderID;

  // Tiles are held on layers, drawn back to front
  for (auto &tileSet : mapData->tileSets)
    tileSetData.push_back(resourceManager->loadTileSet(tileSet.path));

  if (!tileSetData.empty()) {
    // Storing all of the tilesets we will need to load and sending it to our
    // texture instance handle
    std::vector<const char *> buffer;
    for (int layer = 0; layer < tileIds.size(); layer++) {
      buffer.clear();
      for (auto &set : tileSetData)
        buffer.push_back(set->file.c_str());
      if (!buffer.empty())
        instanceHandles.push_back(
            std::make_shared<TileTextureInstance>(buffer));
    }

    loadMap();
    createCollisionMap();
  }
}

void Map::loadMap() {
  tileData.resize(tileIds.size());
  // Tiles are drawn back to front, we track the tileset the tile comes from and
  // additional information about the tilesets themselves so the shader can
  // properly draw the right tile. Each layer is its own instance
  for (int layer = 0; layer < tileIds.size(); layer++) {
    for (int y = 0; y < tileIds[layer].size(); y++) {
      for (int x = 0; x < tileIds[layer][y].size(); x++) {
        glm::mat4 modelMatrix =
            glm::translate(
                glm::mat4(1.f),
                glm::vec3(x * mapData->tileSize, y * mapData->tileSize, 0.0f)) *
            glm::scale(glm::mat4(1.f),
                       glm::vec3(mapData->tileSize, mapData->tileSize, 1.0f));

        int textureIndex =
            static_cast<int>(getTileSetIndex(tileIds[layer][y][x]));
        glm::vec2 originalSize =
            (textureIndex != -1) ? glm::vec2(tileSetData[textureIndex]->width,
                                             tileSetData[textureIndex]->height)
                                 : glm::vec2(0.0f);
        int tilesPerRow =
            (textureIndex != -1) ? tileSetData[textureIndex]->columns : 0;
        int startID =
            (textureIndex != -1) ? mapData->tileSets[textureIndex].startID : 0;
        int tileIndex = tileIds[layer][y][x];

        tileData[layer].push_back({modelMatrix, originalSize, tileIndex,
                                   textureIndex, tilesPerRow, startID});
      }
    }
    instanceHandles[layer]->updateInstanceData(tileData[layer]);
  }
  // The canvas size is the same for every tile atlas, each one growing to match
  // the size of the larget tileset
  glm::vec2 canvasSize =
      glm::vec2(instanceHandles[0]->getTextureArray()->getWidth(),
                instanceHandles[0]->getTextureArray()->getHeight());
  editUniformOnce("uCanvasSize", canvasSize);
}

// The collision map is just a 2D array of 0's and 1's, 1's being collidable and
// 0's not. This may see some changes in the future to properly address special
// tiles, such as portals.
void Map::createCollisionMap() {
  // Match collisionMap to map size
  collisionMap.resize(tileIds[0].size());
  for (int y = 0; y < tileIds[0].size(); ++y) {
    collisionMap[y].resize(tileIds[0][y].size(), 0);
  }

  for (int layer = 0; layer < tileIds.size(); layer++) {
    for (int y = 0; y < tileIds[layer].size(); y++) {
      for (int x = 0; x < tileIds[layer][y].size(); x++) {
        int id = tileIds[layer][y][x];
        size_t tileSetIndex = getTileSetIndex(id);
        if (tileSetIndex == -1)
          collisionMap[y][x] = 0;
        else {
          int startID = mapData->tileSets[tileSetIndex].startID;
          auto &tile = tileSetData[tileSetIndex]->tiles[id - startID];
          if (!tile->walkable)
            collisionMap[y][x] = 1;
        }
      }
    }
  }
}

// Draw each layer in it's own instance
void Map::draw() {
  for (int layer = 0; layer < instanceHandles.size(); layer++) {
    instanceHandles[layer]->draw();
  }
}

/*
Use this function to get the tileSetIndex from a tile ID.
the index of the tileSet is the same index as the texture index used in the
instanceHandle

returns tileSetIndex of the tile id passed. Unless the id is either 0 or
the returned tileSetIndex is out of bounds, returns -1
*/
size_t Map::getTileSetIndex(int id) const {
  // work from the bottom, break if ID > startID
  // If we get a textureIndex of -1 then we are on an empty tile
  size_t tileSetIndex = mapData->tileSets.size() - 1;
  for (; tileSetIndex != -1; --tileSetIndex) {
    if (id >= mapData->tileSets[tileSetIndex].startID)
      break;
  }
  return (tileSetIndex >= mapData->tileSets.size()) ? -1 : tileSetIndex;
}
