#ifndef MAP_H_3AB00D12
#define MAP_H_3AB00D12

#include <vector>
#include <algorithm>
#include <array>
#include <list>
#include <map>
#include <random>
#include "consts.h"
#include "direction.h"

enum class Tile {
  Floor,
  Wall,
  Dust,
  Lamp
};

enum class Source {
  None,
  Dust,
  Lamp,
  Player
};

struct MapData {
  bool lit = false;
  bool wasLit = false;
  size_t dustLife = 0;
  Source lightType = Source::None;
  int lightRadius = 0;
  std::set<coord> litTiles;
  int renderId = -1;
  bool dirtyRender = true;
  bool sign = false;
  std::string text;
};

struct Chunk {
  std::array<Tile, CHUNK_WIDTH*CHUNK_HEIGHT> tiles;
  std::array<MapData, CHUNK_WIDTH*CHUNK_HEIGHT> data;
  int litTiles = 0;
  int x;
  int y;
};

inline void toChunkPos(int x, int y, int& cx, int& cy) {
  cx = std::floor(static_cast<double>(x) / CHUNK_WIDTH);
  cy = std::floor(static_cast<double>(y) / CHUNK_HEIGHT);
}

class Map {
public:

  inline int getLeft() const
  {
    return left_;
  }

  inline int getRight() const
  {
    return left_ + width_;
  }

  inline int getTop() const
  {
    return top_;
  }

  inline int getBottom() const
  {
    return top_ + height_;
  }

  inline int getWidth() const
  {
    return width_;
  }

  inline int getHeight() const
  {
    return height_;
  }

  inline int getTrueX(int x) const
  {
    return (x - left_);
  }

  inline int getTrueY(int y) const
  {
    return (y - top_);
  }

  inline bool inBounds(int x, int y) const
  {
    return (x >= left_) &&
           (x < left_ + width_) &&
           (y >= top_) &&
           (y < top_ + height_);
  }

  inline const Tile& tile(int x, int y) const
  {
    return loadedTiles_[(x - left_) + width_ * (y - top_)];
  }

  inline Tile& tile(int x, int y)
  {
    return const_cast<Tile&>(static_cast<const Map&>(*this).tile(x, y));
  }

  inline const MapData& at(int x, int y) const
  {
    return loaded_[(x - left_) + width_ * (y - top_)];
  }

  inline MapData& at(int x, int y)
  {
    return const_cast<MapData&>(static_cast<const Map&>(*this).at(x, y));
  }

  inline const std::vector<Tile>& tiles() const
  {
    return loadedTiles_;
  }

  inline std::vector<Tile>& tiles()
  {
    return loadedTiles_;
  }

  inline const std::vector<MapData>& data() const
  {
    return loaded_;
  }

  inline std::vector<MapData>& data()
  {
    return loaded_;
  }

  inline int getUnloadedLitTiles() const { return unloadedLitTiles_; }

  void load(int newLeftChunk, int newTopChunk, int newWidthChunks, int newHeightChunks, std::mt19937& rng) {
    // Flush the currently loaded data as long as there is any (this isn't the first load).
    if (!loaded_.empty()) {
      std::vector<size_t> touchedChunks;

      for (int chunkY = 0; chunkY < chunksVert_; chunkY++) {
        for (int chunkX = 0; chunkX < chunksHoriz_; chunkX++) {
          size_t chunkIndex = chunkByPos_.at(leftmostChunk_ + chunkX).at(topmostChunk_ + chunkY);
          touchedChunks.push_back(chunkIndex);

          Chunk& chunk = chunks_.at(chunkIndex);
          chunk.litTiles = 0;

          int startX = chunkX * CHUNK_WIDTH;
          int startY = chunkY * CHUNK_HEIGHT;
          for (int y=0; y<CHUNK_HEIGHT; y++) {
            std::copy(
              std::next(std::begin(loadedTiles_), (chunkY*CHUNK_HEIGHT + y)*width_ + chunkX*CHUNK_WIDTH),
              std::next(std::begin(loadedTiles_), (chunkY*CHUNK_HEIGHT + y)*width_ + (chunkX+1)*CHUNK_WIDTH),
              std::next(std::begin(chunk.tiles), y*CHUNK_WIDTH));

            for (int x=0; x<CHUNK_WIDTH; x++) {
              chunk.data[x+y*CHUNK_WIDTH] = loaded_[startX+x+(startY+y)*width_];
              if (chunk.data[x+y*CHUNK_WIDTH].lit) {
                chunk.litTiles++;
              }
            }
          }

          unloadedLitTiles_ += chunk.litTiles;
        }
      }

      // Destroy any completely unlit chunks.
      /*for (size_t chunkIndex : touchedChunks) {
        if (chunks_[chunkIndex].litTiles == 0) {
          chunkByPos_[chunks_[chunkIndex].x].erase(chunks_[chunkIndex].y);
          freeList_.push_back(chunkIndex);
        }
      }*/
    }

    leftmostChunk_ = newLeftChunk;
    topmostChunk_ = newTopChunk;
    chunksHoriz_ = newWidthChunks;
    chunksVert_ = newHeightChunks;
    left_ = newLeftChunk * CHUNK_WIDTH;
    top_ = newTopChunk * CHUNK_HEIGHT;
    width_ = chunksHoriz_ * CHUNK_WIDTH;
    height_ = chunksVert_ * CHUNK_HEIGHT;
    loaded_ = std::vector<MapData>(width_ * height_);
    loadedTiles_ = std::vector<Tile>(width_ * height_, Tile::Floor);
    dirtyTiles_.clear();
    dirtySet_ = std::vector<bool>(width_ * height_, false);

    // Load in the requested chunks.
    for (int chunkY = 0; chunkY < chunksVert_; chunkY++) {
      for (int chunkX = 0; chunkX < chunksHoriz_; chunkX++) {
        // Instantiate a new chunk if necessary.
        if (!chunkByPos_[leftmostChunk_ + chunkX].count(topmostChunk_ + chunkY)) {
          size_t chunkIndex;
          if (!freeList_.empty()) {
            chunkIndex = freeList_.front();
            freeList_.pop_front();
          } else {
            chunkIndex = chunks_.size();
            chunks_.emplace_back();
          }
          chunkByPos_[leftmostChunk_ + chunkX][topmostChunk_ + chunkY] = chunkIndex;

          Chunk& chunk = chunks_[chunkIndex];
          chunk.x = leftmostChunk_ + chunkX;
          chunk.y = topmostChunk_ + chunkY;
          chunk.litTiles = 0;

          for (Tile& tile : chunk.tiles)
          {
            if (std::bernoulli_distribution(0.5)(rng))
            {
              tile = Tile::Wall;
            } else {
              tile = Tile::Floor;
            }
          }
          for (MapData& md : chunk.data) {
            md.dirtyRender = true;
          }
        }

        size_t chunkIndex = chunkByPos_[leftmostChunk_ + chunkX][topmostChunk_ + chunkY];
        Chunk& chunk = chunks_[chunkIndex];

        for (int y = 0; y < CHUNK_HEIGHT; y++) {
          std::copy(
            std::next(std::begin(chunk.tiles), y*CHUNK_WIDTH),
            std::next(std::begin(chunk.tiles), (y+1)*CHUNK_WIDTH),
            std::next(std::begin(loadedTiles_), (chunkY*CHUNK_HEIGHT + y)*width_ + chunkX*CHUNK_WIDTH));
          std::copy(
            std::next(std::begin(chunk.data), y*CHUNK_WIDTH),
            std::next(std::begin(chunk.data), (y+1)*CHUNK_WIDTH),
            std::next(std::begin(loaded_), (chunkY*CHUNK_HEIGHT + y)*width_ + chunkX*CHUNK_WIDTH));
        }

        unloadedLitTiles_ -= chunk.litTiles;
      }
    }
  }

  void markDirtyAround(int x, int y) {
    if (x > left_) {
      if (y > top_) {
        markDirty(x-1, y-1);
      }
      markDirty(x-1, y  );
      if (y < top_+height_-1) {
        markDirty(x-1, y+1);
      }
    }

    if (y > top_) {
      markDirty(x  , y-1);
    }
    markDirty(x  , y  );
    if (y < top_+height_-1) {
      markDirty(x  , y+1);
    }

    if (x < left_+width_-1) {
      if (y > top_) {
        markDirty(x+1, y-1);
      }
      markDirty(x+1, y  );
      if (y < top_+height_-1) {
        markDirty(x+1, y+1);
      }
    }
  }

  void markDirty(int x, int y) {
    size_t index = (x-left_)+(y-top_)*width_;
    if (!dirtySet_[index]) {
      dirtySet_[index] = true;
      dirtyTiles_.emplace_back(x,y);
    }
  }

  const std::vector<coord> getDirty() const {
    return dirtyTiles_;
  }

  void resetDirty() {
    for (auto [x,y] : dirtyTiles_) {
      dirtySet_[(x-left_)+(y-top_)*width_] = false;
    }
    dirtyTiles_.clear();
  }

private:

  int left_;
  int top_;
  int width_;
  int height_;
  int leftmostChunk_;
  int topmostChunk_;
  int chunksHoriz_;
  int chunksVert_;
  std::vector<Tile> loadedTiles_;
  std::vector<MapData> loaded_;

  std::vector<Chunk> chunks_;
  std::list<size_t> freeList_;
  std::map<int, std::map<int, size_t>> chunkByPos_; // chunkByPos_[X][Y]

  int unloadedLitTiles_ = 0;

  std::vector<coord> dirtyTiles_;
  std::vector<bool> dirtySet_;
};

#endif /* end of include guard: MAP_H_3AB00D12 */