#include <SDL.h>
#include <SDL_image.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 img_error : public std::logic_error {
public:

  img_error() : std::logic_error(IMG_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>;

class surface_deleter {
public:

  void operator()(SDL_Surface* ptr)
  {
    SDL_FreeSurface(ptr);
  }
};

using surface_ptr = std::unique_ptr<SDL_Surface, surface_deleter>;

class texture_deleter {
public:

  void operator()(SDL_Texture* ptr)
  {
    SDL_DestroyTexture(ptr);
  }
};

using texture_ptr = std::unique_ptr<SDL_Texture, texture_deleter>;

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

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

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;
const int RADIUS = 8;

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),
    lightStrength(VIEW_WIDTH*VIEW_HEIGHT, 0.0),
    lightSource(VIEW_WIDTH*VIEW_HEIGHT, Source::None)
  {
  }

  std::vector<Tile> tiles;
  std::vector<bool> lighting;
  std::vector<bool> oldLighting;
  std::vector<double> lightStrength;
  std::vector<Source> lightSource;
  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)
{
  texture_ptr playerFade;
  {
    surface_ptr pfs(IMG_Load("../res/lighting.png"));
    if (!pfs)
    {
      throw img_error();
    }

    playerFade = texture_ptr(SDL_CreateTextureFromSurface(ren, pfs.get()));
  }

  texture_ptr lampFade(
    SDL_CreateTexture(
      ren,
      SDL_PIXELFORMAT_RGBA4444,
      SDL_TEXTUREACCESS_TARGET,
      144,
      144));

  {
    SDL_SetRenderTarget(ren, lampFade.get());

    SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_NONE);
    SDL_SetRenderDrawColor(ren, 255, 255, 255, 0);
    SDL_RenderFillRect(ren, nullptr);

    SDL_RenderCopy(ren, playerFade.get(), nullptr, nullptr);

    SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_MOD);
    SDL_SetRenderDrawColor(ren, 255, 180, 0, 255);
    SDL_RenderFillRect(ren, nullptr);

    SDL_SetRenderTarget(ren, nullptr);
  }

  int darkR = 40;
  int darkG = 40;
  int darkB = 40;

  if (!drawDark)
  {
    darkR = rand() % 255;
    darkG = rand() % 255;
    darkB = rand() % 255;
  }

  SDL_SetRenderDrawColor(ren, darkR, darkG, darkB, 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;
        }*/
        draw = false;
      } else {
        int alpha = map.lightStrength.at(x+y*VIEW_WIDTH) * 255;
        alpha = 255;

        switch (map.tiles.at(x+y*VIEW_WIDTH))
        {
          case Tile::Floor:
          {
            SDL_SetRenderDrawColor(ren, 210, 210, 210, alpha);
            break;
          }

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

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

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

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

        SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
        SDL_RenderFillRect(ren, &rect);


      }
    }
  }

  for (int y = 0; y < VIEW_HEIGHT; y++)
  {
    for (int x = 0; x < VIEW_WIDTH; x++)
    {
      if (map.lightSource.at(x+VIEW_WIDTH*y) != Source::None)
      {
        SDL_Rect fadeRect{x*TILE_WIDTH + (TILE_WIDTH/2) - (144/2), y*TILE_HEIGHT + (TILE_HEIGHT/2) - (144/2), 144, 144};
        if (map.lightSource.at(x+VIEW_WIDTH*y) == Source::Lamp)
        {
          //SDL_SetTextureBlendMode(lampFade.get(), SDL_BLENDMODE_MOD);
          SDL_RenderCopy(ren, lampFade.get(), nullptr, &fadeRect);
          //SDL_SetRenderDrawColor(ren, 255, 180, 0, 50);
          //SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
          //SDL_RenderFillRect(ren, &rect);
        } else if (map.lightSource.at(x+VIEW_WIDTH*y) == Source::Player)
        {
          //SDL_SetTextureBlendMode(playerFade.get(), SDL_BLENDMODE_MOD);
          SDL_RenderCopy(ren, playerFade.get(), nullptr, &fadeRect);
        }

        /*SDL_SetRenderDrawColor(ren, 40, 40, 40, alpha);
        SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_BLEND);
        SDL_RenderFillRect(ren, &rect);*/
      }
    }
  }

  SDL_SetRenderDrawBlendMode(ren, SDL_BLENDMODE_NONE);
  SDL_SetRenderDrawColor(ren, darkR, darkG, darkB, 255);

  for (int y = 0; y < VIEW_HEIGHT; y++)
  {
    for (int x = 0; x < VIEW_WIDTH; x++)
    {
      if (!map.lighting.at(x+VIEW_WIDTH*y))
      {
        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.lightStrength = std::vector<double>(VIEW_WIDTH*VIEW_HEIGHT, 0.0);
  map.lightedSpots = 0;
  map.lightSource = std::vector<Source>(VIEW_WIDTH*VIEW_HEIGHT, Source::None);

  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 dx, int dy, void* source) {
      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.lightedSpots++;
        }

        m.lighting[x+VIEW_WIDTH*y] = true;

        m.lightStrength[x+VIEW_WIDTH*y] = std::max(
          m.lightStrength[x+VIEW_WIDTH*y],
          std::pow(
            std::max(
              0.0,
              1.0 - std::sqrt(dx * dx + dy * dy) / static_cast<double>(RADIUS)),
            1.0/3.0));

        Source ls = *static_cast<Source*>(source);
        if (static_cast<size_t>(ls) > static_cast<size_t>(m.lightSource[x+VIEW_WIDTH*y]))
        {
          //m.lightSource[x+VIEW_WIDTH*y] = ls;
        }
      }
    });

  for (int y = 0; y < VIEW_HEIGHT; y++)
  {
    for (int x = 0; x < VIEW_WIDTH; x++)
    {
      Source ls = Source::None;

      if (player_x == x && player_y == y)
      {
        ls = Source::Player;
      } else if (map.tiles[x+VIEW_WIDTH*y] == Tile::Dust)
      {
        ls = Source::Dust;
      } else if (map.tiles[x+VIEW_WIDTH*y] == Tile::Lamp)
      {
        ls = Source::Lamp;
      }

      if (ls != Source::None)
      {
        fov_circle(fov, static_cast<void*>(&map), static_cast<void*>(&ls), x, y, RADIUS);

        map.lighting[x+VIEW_WIDTH*y] = true;
        map.lightStrength[x+VIEW_WIDTH*y] = 1.0;
        map.lightSource[x+VIEW_WIDTH*y] = ls;
      }
    }
  }
}

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();
  }

  if (IMG_Init(IMG_INIT_PNG) != IMG_INIT_PNG)
  {
    throw img_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 - (RADIUS - 1),
                  player_y - (RADIUS - 1),
                  player_x + RADIUS,
                  player_y + RADIUS);

                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, RADIUS+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;
  }

  IMG_Quit();
  SDL_Quit();

  return 0;
}