#ifndef MAP_H_3AB00D12
#define MAP_H_3AB00D12

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

using coord = std::tuple<int, int>;

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

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

struct MapData {
  Tile tile = Tile::Floor;
  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;
};

struct Chunk {
  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 = x / CHUNK_WIDTH;
  cy = y / CHUNK_HEIGHT;
  if (x < 0) {
    cx *= -1;
    cx--;
  }
  if (y < 0) {
    cy *= -1;
    cy--;
  }
}

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 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<MapData>& data() const
  {
    return loaded_;
  }

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

  /*void resize(int newLeft, int newTop, int newWidth, int newHeight)
  {
    std::vector<T> newData(newWidth * newHeight);

    int winTop = std::max(top_, newTop);
    int winBottom = std::min(top_ + height_, newTop + newHeight);
    int winLeft = std::max(left_, newLeft);
    int winRight = std::min(left_ + width_, newLeft + newWidth);

    for (int y = winTop; y < winBottom; y++)
    {
      std::move(
        std::next(std::begin(data_), (winLeft - left_) + width_ * (y - top_)),
        std::next(std::begin(data_), (winRight - left_) + width_ * (y - top_)),
        std::next(std::begin(newData),
          (winLeft - newLeft) + newWidth * (y - newTop)));
    }

    left_ = newLeft;
    top_ = newTop;
    width_ = newWidth;
    height_ = newHeight;
    data_.swap(newData);
  }*/

  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++) {
            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++;
              }
            }
          }
        }
      }

      // 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_);

    // 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;

          for (MapData& md : chunk.data)
          {
            if (std::bernoulli_distribution(0.5)(rng))
            {
              md.tile = Tile::Wall;
            } else {
              md.tile = Tile::Floor;
            }
            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.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));
        }
      }
    }

  }

private:

  int left_;
  int top_;
  int width_;
  int height_;
  int leftmostChunk_;
  int topmostChunk_;
  int chunksHoriz_;
  int chunksVert_;
  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]
};

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