#include <SDL.h>
#include <stdexcept>
#include <memory>
#include <vector>
#include <random>
#include <fov.h>
#include <deque>
#include <iostream>

class sdl_error : public std::logic_error {
public:

  sdl_error() : std::logic_error(SDL_GetError())
  {
  }
};

class window_deleter {
public:

  void operator()(SDL_Window* ptr)
  {
    SDL_DestroyWindow(ptr);
  }
};

using window_ptr = std::unique_ptr<SDL_Window, window_deleter>;

class renderer_deleter {
public:

  void operator()(SDL_Renderer* ptr)
  {
    SDL_DestroyRenderer(ptr);
  }
};

using renderer_ptr = std::unique_ptr<SDL_Renderer, renderer_deleter>;

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

const int GAME_WIDTH = 640*2;
const int GAME_HEIGHT = 480*2;
const int TILE_WIDTH = 8*2;
const int TILE_HEIGHT = 8*2;
const int VIEW_WIDTH = GAME_WIDTH / TILE_WIDTH;
const int VIEW_HEIGHT = GAME_HEIGHT / TILE_HEIGHT;

struct Input {
  bool left = false;
  bool right = false;
  bool up = false;
  bool down = false;
};

class Map {
public:

  Map() :
    tiles(VIEW_WIDTH*VIEW_HEIGHT, Tile::Floor),
    lighting(VIEW_WIDTH*VIEW_HEIGHT, false)
  {
  }

  std::vector<Tile> tiles;
  std::vector<bool> lighting;
  std::vector<bool> oldLighting;
  int lightedSpots = 0;
};

int player_x = VIEW_WIDTH / 2;
int player_y = VIEW_HEIGHT / 2;

void render(
  SDL_Renderer* ren,
  const Map& map,
  bool drawDark = true)
{
  SDL_SetRenderDrawColor(ren, rand() % 255, rand() % 255, rand() % 255, 255);
  SDL_RenderClear(ren);

  for (int y = 0; y < VIEW_HEIGHT; y++)
  {
    for (int x = 0; x < VIEW_WIDTH; x++)
    {
      bool draw = true;

      if (player_x == x && player_y == y)
      {
        SDL_SetRenderDrawColor(ren, 255, 255, 0, 255);
      } else if (!map.lighting.at(x+VIEW_WIDTH*y))
      {
        if (drawDark)
        {
          SDL_SetRenderDrawColor(ren, 40, 40, 40, 255);
        } else {
          draw = false;
        }
      } else {
        switch (map.tiles.at(x+y*VIEW_WIDTH))
        {
          case Tile::Floor:
          {
            SDL_SetRenderDrawColor(ren, 210, 210, 210, 255);
            break;
          }

          case Tile::Wall:
          {
            SDL_SetRenderDrawColor(ren, 100, 100, 100, 255);
            break;
          }

          case Tile::Dust:
          {
            SDL_SetRenderDrawColor(ren, 128, 40, 255, 255);
            break;
          }

          case Tile::Lamp:
          {
            SDL_SetRenderDrawColor(ren, 0, 255, 255, 255);
            break;
          }
        }
      }

      if (draw)
      {
        SDL_Rect rect{x*TILE_WIDTH, y*TILE_HEIGHT, TILE_WIDTH, TILE_HEIGHT};
        SDL_RenderFillRect(ren, &rect);
      }
    }
  }

  SDL_RenderPresent(ren);
}

void incrementIfSet(Map& map, int& count, int x, int y, int w, int h, Tile val = Tile::Wall)
{
  if ((x >= 0) && (x < w) && (y >= 0) && (y < h) && (map.tiles[x+w*y] == val))
  {
    count++;
  }
}

void tick(Map& map, int x1 = 0, int y1 = 0, int x2 = VIEW_WIDTH, int y2 = VIEW_HEIGHT, bool onlyDark = false)
{
  std::vector<Tile> temp(map.tiles);

  for (int y = std::max(y1, 0); y < std::min(y2, VIEW_HEIGHT); y++)
  {
    for (int x = std::max(x1, 0); x < std::min(x2, VIEW_WIDTH); x++)
    {
      if (onlyDark && map.lighting[x+y*VIEW_WIDTH])
      {
        continue;
      }

      if (map.tiles[x+y*VIEW_WIDTH] == Tile::Lamp)
      {
        continue;
      }

      int count = 0;

      incrementIfSet(map, count, x-1, y-1, VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x-1, y  , VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x-1, y+1, VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x  , y-1, VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x  , y  , VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x  , y+1, VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x+1, y-1, VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x+1, y  , VIEW_WIDTH, VIEW_HEIGHT);
      incrementIfSet(map, count, x+1, y+1, VIEW_WIDTH, VIEW_HEIGHT);

      if (count >= 5)
      {
        temp[x+VIEW_WIDTH*y] = Tile::Wall;
      } else {
        temp[x+VIEW_WIDTH*y] = Tile::Floor;
      }
    }
  }

  map.tiles = temp;
}

void movePlayer(int x, int y, Map& map)
{
  if ((x >= 0) && (x < VIEW_WIDTH) && (y >= 0) && (y < VIEW_HEIGHT) &&
      map.tiles[x+VIEW_WIDTH*y] == Tile::Floor)
  {
    if (map.tiles[player_x+player_y*VIEW_WIDTH] == Tile::Floor)
    {
      map.tiles[player_x+player_y*VIEW_WIDTH] = Tile::Dust;
    }

    player_x = x;
    player_y = y;
  }
}

void setIfValid(Map& map, int x, int y, Tile val)
{
  if ((x >= 0) && (x < VIEW_WIDTH) && (y >= 0) && (y < VIEW_HEIGHT))
  {
    map.tiles[x+VIEW_WIDTH*y] = val;
  }
}

void recalculateLighting(Map& map, fov_settings_type* fov)
{
  map.oldLighting = map.lighting;
  map.lighting = std::vector<bool>(VIEW_WIDTH*VIEW_HEIGHT, false);
  map.lightedSpots = 0;

  fov_settings_set_opacity_test_function(
    fov,
    [] (void* map, int x, int y) {
      return
        x >= 0 &&
        x < VIEW_WIDTH &&
        y >= 0 &&
        y < VIEW_HEIGHT &&
        static_cast<Map*>(map)->tiles.at(x+VIEW_WIDTH*y) == Tile::Wall;
    });

  fov_settings_set_apply_lighting_function(
    fov,
    [] (void* map, int x, int y, int, int, void*) {
      if ((x >= 0) && (x < VIEW_WIDTH) && (y >= 0) && (y < VIEW_HEIGHT))
      {
        Map& m = *static_cast<Map*>(map);
        if (!m.lighting[x+VIEW_WIDTH*y])
        {
          m.lighting[x+VIEW_WIDTH*y] = true;
          m.lightedSpots++;
        }
      }
    });

  for (int y = 0; y < VIEW_HEIGHT; y++)
  {
    for (int x = 0; x < VIEW_WIDTH; x++)
    {
      if ((player_x == x && player_y == y) || map.tiles[x+VIEW_WIDTH*y] == Tile::Dust || map.tiles[x+VIEW_WIDTH*y] == Tile::Lamp)
      {
        fov_circle(fov, static_cast<void*>(&map), nullptr, x, y, 8);
      }

      if (map.tiles[x+VIEW_WIDTH*y] == Tile::Lamp ||
          map.tiles[x+VIEW_WIDTH*y] == Tile::Dust)
      {
        map.lighting[x+VIEW_WIDTH*y] = true;
      }
    }
  }

  map.lighting[player_x+VIEW_WIDTH*player_y] = true;
}

void processKeys(Map& map, const Input& keystate)
{
  int px = player_x;
  int py = player_y;

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

  if (keystate.down)
  {
    py++;
  }

  if (keystate.left)
  {
    px--;
  }

  if (keystate.right)
  {
    px++;
  }

  if (!(player_x == px && player_y == py))
  {
    movePlayer(px, py, map);
  }
}

int main(int, char**)
{
  std::random_device randomEngine;
  std::mt19937 rng(randomEngine());

  if (SDL_Init(SDL_INIT_VIDEO) != 0)
  {
  	throw sdl_error();
  }

  try
  {
    window_ptr win(
      SDL_CreateWindow("Ether", 100, 100, GAME_WIDTH, GAME_HEIGHT, SDL_WINDOW_SHOWN));

    if (!win)
    {
      throw sdl_error();
    }

    renderer_ptr ren(
      SDL_CreateRenderer(
        win.get(),
        -1,
        SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC));

    if (!ren)
    {
      throw sdl_error();
    }

    Map map;

    std::unique_ptr<fov_settings_type> fov(new fov_settings_type());
    fov_settings_init(fov.get());

    for (int y = 0; y < VIEW_HEIGHT; y++)
    {
      for (int x = 0; x < VIEW_WIDTH; x++)
      {
        if (std::bernoulli_distribution(0.5)(rng))
        {
          map.tiles[x+y*VIEW_WIDTH] = Tile::Wall;
        }
      }
    }

    tick(map);
    tick(map);
    tick(map);

    bool quit = false;
    Input keystate;
    SDL_Event e;
    while (!quit)
    {
      //bool input = false;
      //int presses = 0;
      bool pressedSpace = false;
      while (SDL_PollEvent(&e))
      {
        if (e.type == SDL_QUIT)
        {
          quit = true;
        } else if (e.type == SDL_KEYDOWN)
        {
          //presses++;

          switch (e.key.keysym.sym)
          {
            case SDLK_ESCAPE:
            {
              quit = true;
              break;
            }

            case SDLK_SPACE:
            {
              pressedSpace = true;
              //input = true;

              std::deque<std::tuple<int, int>> lamps;
              lamps.emplace_back(player_x, player_y);

              setIfValid(map, player_x  , player_y  , Tile::Lamp);

              for (int i = 0; i < 5; i++)
              {
                processKeys(map, keystate);

                tick(
                  map,
                  player_x - 7,
                  player_y - 7,
                  player_x + 8,
                  player_y + 8);

                render(ren.get(), map, false);
                SDL_Delay(30);
              }

              int lamped = 0;
              while (!lamps.empty())
              {
                lamped++;

                int px, py;
                std::tie(px, py) = lamps.front();
                lamps.pop_front();

                std::unique_ptr<fov_settings_type> dusty(new fov_settings_type);
                fov_settings_set_opacity_test_function(
                  dusty.get(),
                  [] (void* map, int x, int y) {
                    return
                      x >= 0 &&
                      x < VIEW_WIDTH &&
                      y >= 0 &&
                      y < VIEW_HEIGHT &&
                      static_cast<Map*>(map)->tiles.at(x+VIEW_WIDTH*y) == Tile::Wall;
                  });

                fov_settings_set_apply_lighting_function(
                  dusty.get(),
                  [] (void* map, int x, int y, int, int, void* source) {
                    Map& m = *static_cast<Map*>(map);
                    auto& lamps = *static_cast<std::deque<std::pair<int, int>>*>(source);

                    if ((x >= 0) && (x < VIEW_WIDTH) &&
                        (y >= 0) && (y < VIEW_HEIGHT))
                    {
                      if (m.tiles[x+VIEW_WIDTH*y] == Tile::Floor)
                      {
                        m.tiles[x+VIEW_WIDTH*y] = Tile::Dust;
                      } else if (m.tiles[x+VIEW_WIDTH*y] == Tile::Lamp)
                      {
                        m.tiles[x+VIEW_WIDTH*y] = Tile::Dust;
                        lamps.emplace_back(x, y);
                      }
                    }
                  });

                fov_circle(dusty.get(), static_cast<void*>(&map), static_cast<void*>(&lamps), px, py, 8+lamped*lamped);

                render(ren.get(), map, false);
                SDL_Delay(50);
              }

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

      bool input = keystate.left || keystate.right || keystate.up || keystate.down || pressedSpace;

      if (input)
      {
        for (int y = 0; y < VIEW_HEIGHT; y++)
        {
          for (int x = 0; x < VIEW_WIDTH; x++)
          {
            if (map.tiles[x+y*VIEW_WIDTH] == Tile::Dust)
            {
              map.tiles[x+y*VIEW_WIDTH] = Tile::Floor;
            }
          }
        }
      }

      processKeys(map, keystate);
      recalculateLighting(map, fov.get());

      if (input)
      {
        for (int y = 0; y < VIEW_HEIGHT; y++)
        {
          for (int x = 0; x < VIEW_WIDTH; x++)
          {
            if (!map.lighting[x+y*VIEW_WIDTH] && map.oldLighting[x+y*VIEW_WIDTH])
            {
              if (std::bernoulli_distribution(0.5)(rng))
              {
                map.tiles[x+y*VIEW_WIDTH] = Tile::Wall;
              } else {
                map.tiles[x+y*VIEW_WIDTH] = Tile::Floor;
              }
            }
          }
        }

        tick(map, 0, 0, VIEW_WIDTH, VIEW_HEIGHT, true);
        tick(map, 0, 0, VIEW_WIDTH, VIEW_HEIGHT, true);
        tick(map, 0, 0, VIEW_WIDTH, VIEW_HEIGHT, true);
      }

      render(ren.get(), map, true);
      SDL_Delay(50);
    }
  } catch (const sdl_error& ex)
  {
    std::cout << "SDL error (" << ex.what() << ")" << std::endl;
  }

  SDL_Quit();

  return 0;
}