#include "game.h"
#include <vector>
#include <fov.h>
#include <iostream>
#include <fstream>
#include "util.h"
#include "renderer.h"
#include "consts.h"

Game::Game(std::mt19937& rng, Muxer& muxer, Renderer& renderer) :
  rng(rng),
  muxer(muxer),
  sign(renderer.getFont())
{
  losePopLampTimer.accumulate(losePopLampTimer.getDt());

  loadMap();
  tick();
  tick();
  tick();

  for (int y = -1; y <= 1; y++)
  {
    for (int x = -1; x <= 1; x++)
    {
      map.tile(x,y) = Tile::Floor;
    }
  }

  tick();

  std::ifstream textFile("../res/childoflight.txt");
  std::string line;
  while (std::getline(textFile, line)) {
    signTexts.push_back(line);
  }
}

inline bool isTileSetOrNotLit(const Map& map, int x, int y)
{
  return (map.inBounds(x, y) && (map.tile(x,y) == Tile::Wall || !map.at(x,y).lit));
}

inline bool isTileSet(const Map& map, int x, int y, Tile val = Tile::Wall)
{
  return (map.inBounds(x, y) && map.tile(x,y) == val);
}

inline void incrementIfSet(const Game& game, int& count, int x, int y, Tile val = Tile::Wall)
{
  if (isTileSet(game.map, x, y, val))
  {
    count++;
  }
}

inline int getZoomLevel(const Game& game) {
  return game.curZoom - INIT_ZOOM;
}

inline void tickIndividual(Game& game, std::vector<Tile>& mapDoubleBuffer, int x, int y, bool onlyDark) {
  if (onlyDark && game.map.at(x,y).lit)
  {
    return;
  }

  if (game.map.tile(x,y) == Tile::Lamp)
  {
    return;
  }

  int count = 0;

  incrementIfSet(game, count, x-1, y-1);
  incrementIfSet(game, count, x-1, y  );
  incrementIfSet(game, count, x-1, y+1);
  incrementIfSet(game, count, x  , y-1);
  incrementIfSet(game, count, x  , y  );
  incrementIfSet(game, count, x  , y+1);
  incrementIfSet(game, count, x+1, y-1);
  incrementIfSet(game, count, x+1, y  );
  incrementIfSet(game, count, x+1, y+1);

  int tempIndex = game.map.getTrueX(x) + game.map.getTrueY(y) * game.map.getWidth();
  if (count >= 5)
  {
    mapDoubleBuffer[tempIndex] = Tile::Wall;
  } else {
    mapDoubleBuffer[tempIndex] = Tile::Floor;
  }

  if (mapDoubleBuffer[tempIndex] != game.map.tile(x,y)) {
    game.map.at(x,y).dirtyRender = true;
    game.dirtyRender = true;
    game.map.markDirtyAround(x,y);
  }
}

void Game::tickDirty(bool onlyDark) {
  mapDoubleBuffer = map.tiles();
  std::vector<coord> dirty = map.getDirty();
  map.resetDirty();

  for (auto [x,y] : dirty) {
    tickIndividual(*this, mapDoubleBuffer, x, y, onlyDark);
  }

  std::swap(map.tiles(), mapDoubleBuffer);
}

void Game::tickOuter(bool onlyDark) {
  mapDoubleBuffer = map.tiles();
  map.resetDirty();

  for (int y = 0; y < CHUNK_HEIGHT + 2; y++) {
    for (int x = map.getLeft(); x < map.getRight(); x++) {
      tickIndividual(*this, mapDoubleBuffer, x, map.getTop() + y, onlyDark);
      tickIndividual(*this, mapDoubleBuffer, x, map.getBottom() - 1 - y, onlyDark);
    }
  }

  for (int x = 0; x < CHUNK_WIDTH + 2; x++) {
    for (int y = map.getTop() + CHUNK_HEIGHT + 2; y < map.getBottom() - CHUNK_HEIGHT - 2; y++) {
      tickIndividual(*this, mapDoubleBuffer, map.getLeft() + x, y, onlyDark);
      tickIndividual(*this, mapDoubleBuffer, map.getRight() - 1 - x, y, onlyDark);
    }
  }

  std::swap(map.tiles(), mapDoubleBuffer);
}

void Game::tick(
  int x1,
  int y1,
  int x2,
  int y2,
  bool invert,
  bool onlyDark)
{
  mapDoubleBuffer = map.tiles();
  map.resetDirty();

  for (int y = map.getTop(); y < map.getBottom(); y++)
  {
    for (int x = map.getLeft(); x < map.getRight(); x++)
    {
      if (invert == (x >= x1 && x < x2 && y >= y1 && y < y2))
      {
        continue;
      }

      tickIndividual(*this, mapDoubleBuffer, x, y, onlyDark);
    }
  }

  std::swap(map.tiles(), mapDoubleBuffer);
}

void Game::tick(bool onlyDark)
{
  tick(
    map.getLeft(),
    map.getTop(),
    map.getRight(),
    map.getBottom(),
    false,
    onlyDark);
}

bool Game::movePlayer(int x, int y)
{
  if (map.tile(x,y) == Tile::Floor && !map.at(x,y).sign)
  {
    if (map.tile(player_x, player_y) == Tile::Floor)
    {
      map.tile(player_x, player_y) = Tile::Dust;
      map.at(player_x, player_y).dustLife = 1;
      numDust++;
    }

    player_oldx = player_x;
    player_oldy = player_y;
    player_x = x;
    player_y = y;
    muxer.setPlayerLoc(x, y);
    moving = true;
    moveProgress.start(1000/6);
    dirtyLighting = true;

    std::cout << player_x << "," << player_y << std::endl;

    int chunkX, chunkY, old_chunkX, old_chunkY;
    toChunkPos(player_x, player_y, chunkX, chunkY);
    toChunkPos(player_oldx, player_oldy, old_chunkX, old_chunkY);
    if ((chunkX != old_chunkX) || (chunkY != old_chunkY)) {
      loadMap();
    }

    return true;
  } else {
    if (!alreadyBumped) {
      muxer.playSoundAtPosition("bump", x, y);
      alreadyBumped = true;
      bumpCooldown.reset();
    }

    return false;
  }
}

void Game::recalculateLighting()
{
  litSpots = 0;
  dirtyRender = true;

  for (int y=map.getTop(); y<map.getBottom(); y++) {
    for (int x=map.getLeft(); x<map.getRight(); x++) {
      map.at(x,y).wasLit = map.at(x,y).lit;
      map.at(x,y).lit = false;
      map.at(x,y).litTiles.clear();

      if (map.tile(x,y) == Tile::Wall) {
        map.at(x,y).dirtyRender = true;
      }
    }
  }

  fov_settings_type fov;
  fov_settings_init(&fov);

  fov_settings_set_opacity_test_function(
    &fov,
    [] (void* data, int x, int y) {
      Game& game = *static_cast<Game*>(data);

      return game.map.inBounds(x,y) && game.map.tile(x,y) == Tile::Wall;
    });

  fov_settings_set_apply_lighting_function(
    &fov,
    [] (void* data, int x, int y, int dx, int dy, void* source) {
      Game& game = *static_cast<Game*>(data);

      if (game.map.inBounds(x, y))
      {
        MapData& sourceData = *static_cast<MapData*>(source);
        double lightRadius = static_cast<double>(sourceData.lightRadius);

        if (!game.map.at(x,y).lit)
        {
          game.litSpots++;
        }

        game.map.at(x,y).lit = true;

        sourceData.litTiles.emplace(x,y);
      }
    });

  for (int y = map.getTop(); y < map.getBottom(); y++)
  {
    for (int x = map.getLeft(); x < map.getRight(); x++)
    {
      Source ls = Source::None;
      int lightRadius;

      if (renderPlayer && player_x == x && player_y == y)
      {
        ls = Source::Player;
        lightRadius = RADIUS;
      } else if (map.tile(x,y) == Tile::Dust)
      {
        ls = Source::Dust;
        lightRadius = 2;
      } else if (map.tile(x,y) == Tile::Lamp)
      {
        ls = Source::Lamp;
        lightRadius = RADIUS;
      }

      map.at(x,y).lightType = ls;

      if (ls != Source::None)
      {
        map.at(x,y).lightRadius = lightRadius;
        map.at(x,y).litTiles.emplace(x,y);

        fov_circle(
          &fov,
          static_cast<void*>(this),
          static_cast<void*>(&map.at(x,y)),
          x,
          y,
          lightRadius);

        map.at(x,y).lit = true;
      }
    }
  }

  litSpots += map.getUnloadedLitTiles();
  dirtyLighting = false;
}

void Game::recalculateRender() {
  for (int y = map.getTop(); y < map.getBottom(); y++)
  {
    for (int x = map.getLeft(); x < map.getRight(); x++)
    {
      if (map.at(x,y).dirtyRender) {
        map.at(x,y).dirtyRender = false;
        map.at(x,y).renderId = -1;

        if (map.tile(x,y) == Tile::Floor) {
          int renderDesc = 0;
          if (isTileSetOrNotLit(map, x-1, y-1)) renderDesc |= (1 << 7);
          if (isTileSetOrNotLit(map, x  , y-1)) renderDesc |= (1 << 6);
          if (isTileSetOrNotLit(map, x+1, y-1)) renderDesc |= (1 << 5);
          if (isTileSetOrNotLit(map, x+1, y  )) renderDesc |= (1 << 4);
          if (isTileSetOrNotLit(map, x+1, y+1)) renderDesc |= (1 << 3);
          if (isTileSetOrNotLit(map, x  , y+1)) renderDesc |= (1 << 2);
          if (isTileSetOrNotLit(map, x-1, y+1)) renderDesc |= (1 << 1);
          if (isTileSetOrNotLit(map, x-1, y  )) renderDesc |= (1 << 0);

          if (/*renderDesc == 0 && */map.at(x,y).sign) {
            map.at(x,y).renderId = TilesetIndex(24, 13);
          } else if (std::bernoulli_distribution(0.05)(rng)) {
            static const std::vector<int> furnishings {
              TilesetIndex(20, 16),
              TilesetIndex(21, 2),
              TilesetIndex(22, 2),
              TilesetIndex(21, 3),
              TilesetIndex(22, 3)};

            if (/*renderDesc == 0 &&*/ !(x == player_x && y == player_y) && std::bernoulli_distribution(0.005)(rng)) {
              map.at(x,y).renderId = TilesetIndex(24, 13);
              map.at(x,y).sign = true;
            } else {
              map.at(x,y).renderId = furnishings.at(std::uniform_int_distribution<int>(0, furnishings.size()-1)(rng));
              map.at(x,y).sign = false;
            }
          } else {
            map.at(x,y).sign = false;
          }
        } else if (map.tile(x,y) == Tile::Wall) {
          static bool initWalls = false;
          static std::vector<int> wallRenders(256, TilesetIndex(21, 12));
          if (!initWalls) {
            initWalls = true;

            // Start in top left and go clockwise.
            wallRenders[0b00011100] = TilesetIndex(16, 14);
            wallRenders[0b00111100] = TilesetIndex(16, 14);
            wallRenders[0b00011110] = TilesetIndex(16, 14);
            wallRenders[0b00111110] = TilesetIndex(16, 14);
            wallRenders[0b00111101] = TilesetIndex(16, 14);
            wallRenders[0b10011101] = TilesetIndex(16, 14);

            wallRenders[0b00011111] = TilesetIndex(17, 14);
            wallRenders[0b10011111] = TilesetIndex(17, 14);
            wallRenders[0b00111111] = TilesetIndex(17, 14);
            wallRenders[0b10111111] = TilesetIndex(17, 14);
            wallRenders[0b01011111] = TilesetIndex(17, 14);

            wallRenders[0b00000111] = TilesetIndex(18, 14);
            wallRenders[0b00001111] = TilesetIndex(18, 14);
            wallRenders[0b10000111] = TilesetIndex(18, 14);
            wallRenders[0b10001111] = TilesetIndex(18, 14);
            wallRenders[0b10101111] = TilesetIndex(18, 14);

            wallRenders[0b01111100] = TilesetIndex(16, 15);
            wallRenders[0b11111100] = TilesetIndex(16, 15);
            wallRenders[0b01111110] = TilesetIndex(16, 15);
            wallRenders[0b11111110] = TilesetIndex(16, 15);
            wallRenders[0b01111101] = TilesetIndex(16, 15);

            wallRenders[0b11000111] = TilesetIndex(18, 15);
            wallRenders[0b11001111] = TilesetIndex(18, 15);
            wallRenders[0b11100111] = TilesetIndex(18, 15);
            wallRenders[0b11101111] = TilesetIndex(18, 15);
            wallRenders[0b11010111] = TilesetIndex(18, 15);

            wallRenders[0b01110000] = TilesetIndex(16, 16);
            wallRenders[0b01111000] = TilesetIndex(16, 16);
            wallRenders[0b11110000] = TilesetIndex(16, 16);
            wallRenders[0b11111000] = TilesetIndex(16, 16);
            wallRenders[0b11111010] = TilesetIndex(16, 16);

            wallRenders[0b11110001] = TilesetIndex(17, 16);
            wallRenders[0b11110011] = TilesetIndex(17, 16);
            wallRenders[0b11111001] = TilesetIndex(17, 16);
            wallRenders[0b11111011] = TilesetIndex(17, 16);
            wallRenders[0b11110101] = TilesetIndex(17, 16);

            wallRenders[0b11000001] = TilesetIndex(18, 16);
            wallRenders[0b11000011] = TilesetIndex(18, 16);
            wallRenders[0b11100001] = TilesetIndex(18, 16);
            wallRenders[0b11100011] = TilesetIndex(18, 16);
            wallRenders[0b11001001] = TilesetIndex(18, 16);
            wallRenders[0b11011001] = TilesetIndex(18, 16);


            wallRenders[0b11110111] = TilesetIndex(21, 14);
            wallRenders[0b11111101] = TilesetIndex(22, 14);
            wallRenders[0b11011111] = TilesetIndex(21, 15);
            wallRenders[0b01111111] = TilesetIndex(22, 15);
          }

          int renderDesc = 0;
          if (isTileSetOrNotLit(map, x-1, y-1)) renderDesc |= (1 << 7);
          if (isTileSetOrNotLit(map, x  , y-1)) renderDesc |= (1 << 6);
          if (isTileSetOrNotLit(map, x+1, y-1)) renderDesc |= (1 << 5);
          if (isTileSetOrNotLit(map, x+1, y  )) renderDesc |= (1 << 4);
          if (isTileSetOrNotLit(map, x+1, y+1)) renderDesc |= (1 << 3);
          if (isTileSetOrNotLit(map, x  , y+1)) renderDesc |= (1 << 2);
          if (isTileSetOrNotLit(map, x-1, y+1)) renderDesc |= (1 << 1);
          if (isTileSetOrNotLit(map, x-1, y  )) renderDesc |= (1 << 0);

          map.at(x,y).renderId = wallRenders.at(renderDesc);

          if (wallRenders.at(renderDesc) == TilesetIndex(21, 12) && renderDesc != 255) {
            //std::cout << renderDesc << std::endl;
            /*std::cout << ((renderDesc & (1 << 7)) ? 'X' : 'O');
            std::cout << ((renderDesc & (1 << 6)) ? 'X' : 'O');
            std::cout << ((renderDesc & (1 << 5)) ? 'X' : 'O');
            std::cout << std::endl;
            std::cout << ((renderDesc & (1 << 0)) ? 'X' : 'O');
            std::cout << ' ';
            std::cout << ((renderDesc & (1 << 4)) ? 'X' : 'O');
            std::cout << " " << renderDesc << std::endl;
            std::cout << ((renderDesc & (1 << 1)) ? 'X' : 'O');
            std::cout << ((renderDesc & (1 << 2)) ? 'X' : 'O');
            std::cout << ((renderDesc & (1 << 3)) ? 'X' : 'O');
            std::cout << std::endl;*/
          }
        }
      }
    }
  }

  dirtyRender = false;
}

bool Game::processKeys(const Input& keystate)
{
  int px = player_x;
  int py = player_y;
  Direction dir = Direction::up;

  if (keystate.up)
  {
    py--;
  }

  if (keystate.down)
  {
    py++;
    dir = Direction::down;
  }

  if (keystate.left)
  {
    px--;
    dir = Direction::left;
  }

  if (keystate.right)
  {
    px++;
    dir = Direction::right;
  }

  if (!(player_x == px && player_y == py))
  {
    playerAnim.setAnimation("walk");
    playerAnim.setDirection(dir);

    bool succeeds = movePlayer(px, py);
    if (!succeeds && px != player_x) {
      succeeds = movePlayer(px, player_y);
    }
    if (!succeeds && py != player_y) {
      succeeds = movePlayer(player_x, py);
    }
    return succeeds;
  } else {
    return false;
  }
}

void Game::kickUpDust(int x, int y, size_t chain)
{
  Kickup dk;
  dk.x = x;
  dk.y = y;
  dk.chain = chain;
  dk.cur = 0;
  dk.radius = RADIUS + (chain + 1) * (chain + 1);
  dk.front.emplace(x, y);
  dk.done.emplace(x, y);

  kickups.push_back(dk);
}

void Game::popLamp(int x, int y, size_t chain)
{
  muxer.playSoundAtPosition("pop", x, y);

  if (map.tile(x,y) == Tile::Lamp)
  {
    numLamps--;
  }

  map.tile(x,y) = Tile::Dust;
  map.at(x,y).dustLife = 2;
  numDust++;
  dirtyLighting = true;

  kickUpDust(x, y, chain);
}

void Game::processKickup()
{
  for (Kickup& kickup : kickups)
  {
    kickup.cur++;

    std::set<coord> newFront;
    for (const coord& xy : kickup.front)
    {
      auto processDir = [&] (int x, int y) {
        coord c {x,y};

        if (map.inBounds(x,y) &&
            (map.tile(x,y) == Tile::Floor || map.tile(x,y) == Tile::Lamp) &&
            !kickup.done.count(c))
        {
          newFront.insert(c);
          kickup.done.insert(c);

          if (map.tile(x,y) == Tile::Floor)
          {
            map.tile(x,y) = Tile::Dust;
            map.at(x,y).dustLife = 2;
            numDust++;
            dirtyLighting = true;
          } else if (map.tile(x,y) == Tile::Lamp)
          {
            popLamp(x, y, kickup.chain + 1);
          }
        }
      };

      processDir(std::get<0>(xy) - 1, std::get<1>(xy)    );
      processDir(std::get<0>(xy) + 1, std::get<1>(xy)    );
      processDir(std::get<0>(xy)    , std::get<1>(xy) - 1);
      processDir(std::get<0>(xy)    , std::get<1>(xy) + 1);

      if (std::bernoulli_distribution(0.5)(rng))
      {
        processDir(std::get<0>(xy) - 1, std::get<1>(xy) - 1);
      }

      if (std::bernoulli_distribution(0.5)(rng))
      {
        processDir(std::get<0>(xy) - 1, std::get<1>(xy) + 1);
      }

      if (std::bernoulli_distribution(0.5)(rng))
      {
        processDir(std::get<0>(xy) + 1, std::get<1>(xy) - 1);
      }

      if (std::bernoulli_distribution(0.5)(rng))
      {
        processDir(std::get<0>(xy) + 1, std::get<1>(xy) + 1);
      }
    }

    kickup.front.swap(newFront);
  }

  erase_if(
    kickups,
    [] (const Kickup& kickup) {
      return kickup.cur == kickup.radius;
    });
}

void Game::loadMap() {
  double zoomBasis = getZoomBasis();
  int newChunksHoriz = std::ceil(zoomBasis * ZOOM_X_FACTOR / CHUNK_WIDTH) + 4;
  int newChunksVert = std::ceil(zoomBasis * ZOOM_Y_FACTOR / CHUNK_HEIGHT) + 4;
  int curPlayerChunkX, curPlayerChunkY;
  toChunkPos(player_x, player_y, curPlayerChunkX, curPlayerChunkY);

  map.load(
    curPlayerChunkX - newChunksHoriz / 2,
    curPlayerChunkY - newChunksVert / 2,
    newChunksHoriz,
    newChunksVert,
    rng);

  ticksNeeded = 3;
  dirtyLighting = true;
  dirtyRender = true;
}

void Game::setZoom(size_t zoom)
{
  if (zoom == curZoom || zooming)
  {
    return;
  }

  /*zoomProgress = 0;
  zoomLength = std::abs(static_cast<long>(zoom - curZoom)) * TILE_WIDTH;*/
  zoomProgress.start(62 * std::abs(static_cast<long>(zoom) - curZoom) * TILE_WIDTH);
  oldZoom = curZoom;
  curZoom = zoom;
  zooming = true;
  loadMap();

  int zoomLevel = getZoomLevel(*this);
  if (zoomLevel == 0) {
    muxer.setMusicLevel(0);
  } else if (zoomLevel < 3) {
    muxer.setMusicLevel(1);
  } else if (zoomLevel < 5) {
    muxer.setMusicLevel(2);
  } else if (zoomLevel < 7) {
    muxer.setMusicLevel(3);
  } else {
    muxer.setMusicLevel(4);
  }
}

void Game::performDash() {
  if (map.tile(player_x, player_y) == Tile::Floor) {
    std::vector<coord> freeSpaces;

    auto addIfFree = [&] (int x, int y) {
      if (map.inBounds(x,y) && map.tile(x,y) == Tile::Floor)
      {
        freeSpaces.emplace_back(x, y);
      }
    };

    addIfFree(player_x - 1, player_y - 1);
    addIfFree(player_x    , player_y - 1);
    addIfFree(player_x + 1, player_y - 1);
    addIfFree(player_x - 1, player_y    );
    addIfFree(player_x + 1, player_y    );
    addIfFree(player_x - 1, player_y + 1);
    addIfFree(player_x    , player_y + 1);
    addIfFree(player_x + 1, player_y + 1);

    if (!freeSpaces.empty())
    {
      map.tile(player_x, player_y) = Tile::Lamp;
      numLamps++;
      dirtyLighting = true;
      kickUpDust(player_x, player_y, 0);
      muxer.playSoundAtPosition("drop", player_x, player_y);

      if (firstInput)
      {
        for (int i = 0; i < 5; i++)
        {
          if (!processKeys(lastInput))
          {
            std::uniform_int_distribution<int> freeDist(0, freeSpaces.size() - 1);

            int freeIndex = freeDist(rng);
            coord& moveTo = freeSpaces[freeIndex];

            movePlayer(std::get<0>(moveTo), std::get<1>(moveTo));
          }

          tick(
            player_x - (RADIUS - 1),
            player_y - (RADIUS - 1),
            player_x + RADIUS,
            player_y + RADIUS);
        }
      } else {
        std::uniform_int_distribution<int> freeDist(0, freeSpaces.size() - 1);

        int freeIndex = freeDist(rng);
        coord& moveTo = freeSpaces[freeIndex];

        movePlayer(std::get<0>(moveTo), std::get<1>(moveTo));
      }

      //muxer.playSoundAtPosition("dash", player_x, player_y);
    }
  }
}

void Game::updatePlaying(size_t frameTime) {
  SDL_Event e;

  while (SDL_PollEvent(&e))
  {
    if (e.type == SDL_QUIT)
    {
      if (losing != LoseState::None)
      {
        quit = true;
      } else {
        losing = LoseState::PoppingLamps;
        muxer.stopMusic();
      }
    } else if (e.type == SDL_KEYDOWN)
    {
      switch (e.key.keysym.sym)
      {
        case SDLK_ESCAPE:
        {
          if (losing != LoseState::None)
          {
            quit = true;
          } else {
            losing = LoseState::PoppingLamps;
            muxer.stopMusic();
          }

          break;
        }

        case SDLK_SPACE:
        {
          if (losing == LoseState::None)
          {
            auto [lookX, lookY] = coordInDirection(player_x, player_y, playerAnim.getDirection());
            MapData& lookTile = map.at(lookX, lookY);
            if (moving) {
              if (!lookTile.sign) {
                queueDash = true;
              }
            } else if (lookTile.sign) {
              if (lookTile.text.empty()) {
                int lineToRead = nextSignIndex++;
                if (nextSignIndex >= signTexts.size()) {
                  nextSignIndex = 0;
                }
                lookTile.text = signTexts[lineToRead];
              }

              sign.displayMessage(lookTile.text);
            } else {
              performDash();
            }
          }

          break;
        }
      }
    }
  }

  const Uint8* state = SDL_GetKeyboardState(NULL);
  keystate.left = state[SDL_SCANCODE_LEFT];
  keystate.right = state[SDL_SCANCODE_RIGHT];
  keystate.up = state[SDL_SCANCODE_UP];
  keystate.down = state[SDL_SCANCODE_DOWN];

  bumpCooldown.accumulate(frameTime);
  if (alreadyBumped && keystate != lastInput && bumpCooldown.step()) {
    alreadyBumped = false;
  }

  if (queueDash && !moving) {
    queueDash = false;
    performDash();
  }

  if (keystate.left || keystate.right || keystate.up || keystate.down)
  {
    firstInput = true;
    lastInput = keystate;
  } else if (losing == LoseState::None) {
    playerAnim.setAnimation("still");
  }

  dustTimer.accumulate(frameTime);
  inputTimer.accumulate(frameTime);

  while (dustTimer.step())
  {
    numDust = 0;

    for (int y=map.getTop(); y<map.getBottom(); y++) {
      for (int x=map.getLeft(); x<map.getRight(); x++) {
        if (map.tile(x,y) == Tile::Dust)
        {
          map.at(x,y).dustLife--;

          if (map.at(x,y).dustLife <= 0)
          {
            map.tile(x,y) = Tile::Floor;
            dirtyLighting = true;
          } else {
            numDust++;
          }
        }
      }
    }

    processKickup();
  }

  switch (losing)
  {
    case LoseState::None:
    {
      if (moving) {
        moveProgress.tick(frameTime);
        if (moveProgress.isComplete()) {
          moving = false;
        }
      }

      while (inputTimer.step())
      {
        if (!moving) {
          processKeys(keystate);
        }
      }

      break;
    }

    case LoseState::PoppingLamps:
    {
      if (numLamps == 0)
      {
        if (numDust == 0)
        {
          losing = LoseState::PoppingPlayer;
        }
      } else {
        losePopLampTimer.accumulate(frameTime);

        while (losePopLampTimer.step())
        {
          std::vector<std::tuple<int, int>> lamps;

          for (int y = map.getTop(); y < map.getBottom(); y++)
          {
            for (int x = map.getLeft(); x < map.getRight(); x++)
            {
              if (map.tile(x,y) == Tile::Lamp)
              {
                lamps.emplace_back(x, y);
              }
            }
          }

          std::uniform_int_distribution<int> lampDist(0, lamps.size() - 1);
          std::tuple<int, int> popPos = lamps[lampDist(rng)];

          popLamp(std::get<0>(popPos), std::get<1>(popPos), 1);
        }
      }

      break;
    }

    case LoseState::PoppingPlayer:
    {
      losePopPlayerTimer.accumulate(frameTime);

      if (losePopPlayerTimer.step())
      {
        popLamp(player_x, player_y, 10);
        renderPlayer = false;

        losing = LoseState::Outro;
      }

      break;
    }

    case LoseState::Outro:
    {
      if (numDust == 0)
      {
        quit = true;
      }

      break;
    }
  }

  switch (signInstructionState) {
    case SignInstructionState::Hidden: {
      auto [lookX, lookY] = coordInDirection(player_x, player_y, playerAnim.getDirection());
      if (losing == LoseState::None && map.at(lookX, lookY).sign) {
        signInstructionState = SignInstructionState::FadingIn;
        signFade.start(1000);
      }

      break;
    }
    case SignInstructionState::FadingIn: {
      signFade.tick(frameTime);
      if (signFade.isComplete()) {
        signInstructionState = SignInstructionState::Visible;
      }

      break;
    }
    case SignInstructionState::Visible: {
      auto [lookX, lookY] = coordInDirection(player_x, player_y, playerAnim.getDirection());
      if (!map.at(lookX, lookY).sign || losing != LoseState::None) {
        signInstructionState = SignInstructionState::FadingOut;
        signFade.start(1000);
      }

      break;
    }
    case SignInstructionState::FadingOut: {
      signFade.tick(frameTime);
      if (signFade.isComplete()) {
        signInstructionState = SignInstructionState::Hidden;
      }

      break;
    }
  }

  if (ticksNeeded > 0) {
    gradualTickTimer.accumulate(frameTime);
    if (gradualTickTimer.step()) {
      ticksNeeded--;
      tickOuter();
    }
  }

  if (dirtyLighting)
  {
    recalculateLighting();

    for (int y = map.getTop(); y < map.getBottom(); y++)
    {
      for (int x = map.getLeft(); x < map.getRight(); x++)
      {
        if (!map.at(x,y).lit && map.at(x,y).wasLit)
        {
          Tile oldTile = map.tile(x,y);
          if (std::bernoulli_distribution(0.5)(rng))
          {
            map.tile(x,y) = Tile::Wall;
          } else {
            map.tile(x,y) = Tile::Floor;
          }
          if (map.tile(x,y) != oldTile) {
            map.at(x,y).dirtyRender = true;
            map.markDirtyAround(x,y);
          }
        }
      }
    }

    tickDirty(true);
    tickDirty(true);
    tickDirty(true);

    // TODO: better zoom algorithm
    setZoom(litSpots / 1500 + INIT_ZOOM);
  }

  if (dirtyRender) {
    recalculateRender();
  }

  /*zoomTimer.accumulate(frameTime);
  while (zoomTimer.step())
  {
    if (zooming)
    {
      zoomProgress++;

      if (zoomProgress == zoomLength)
      {
        zooming = false;
      }
    }
  }*/
  if (zooming) {
    zoomProgress.tick(frameTime);
    if (zoomProgress.isComplete()) {
      zooming = false;
    }
  }

  playerAnim.update(frameTime);
}

void Game::update(size_t frameTime) {
  if (sign.signDisplayState != SignInstructionState::Hidden) {
    sign.update(frameTime, *this);
  } else {
    updatePlaying(frameTime);
  }
}