#include "raycaster.hpp"
#include "dbc.hpp"
#include "matrix.hpp"
#include <algorithm>
#include <cmath>
#include <cstdlib>
#include <fmt/core.h>
#include <memory>
#include <numbers>
#include "components.hpp"

using namespace fmt;
using std::make_unique;

union ColorConv {
  struct {
    uint8_t r;
    uint8_t g;
    uint8_t b;
    uint8_t a;
  } as_color;
  uint32_t as_int;
};

inline uint32_t dumb_lighting(uint32_t pixel, double distance) {
  if(distance < 1.0) return pixel;

  ColorConv conv{.as_int=pixel};
  conv.as_color.r /= distance;
  conv.as_color.g /= distance;
  conv.as_color.b /= distance;

  return conv.as_int;
}

Raycaster::Raycaster(TexturePack &textures, int width, int height) :
  $textures(textures),
  $view_texture({(unsigned int)width, (unsigned int)height}),
  $view_sprite($view_texture),
  $width(width), $height(height),
  $zbuffer(width),
  $anim(256, 256, 10, "assets/monster-1.ogg")
{
  $view_sprite.setPosition({0, 0});
  $pixels = make_unique<RGBA[]>($width * $height);
  $view_texture.setSmooth(false);
}

void Raycaster::set_position(int x, int y) {
  $screen_pos_x = x;
  $screen_pos_y = y;
  $view_sprite.setPosition({(float)x, (float)y});
}

void Raycaster::position_camera(float player_x, float player_y) {
  // x and y start position
  $pos_x = player_x;
  $pos_y = player_y;
}

void Raycaster::draw_pixel_buffer() {
  $view_texture.update((uint8_t *)$pixels.get(), {(unsigned int)$width, (unsigned int)$height}, {0, 0});
}

void Raycaster::sprite_casting(sf::RenderTarget &target) {
  constexpr const int texture_width = TEXTURE_WIDTH;
  constexpr const int texture_height = TEXTURE_HEIGHT;
  constexpr const int half_height = TEXTURE_HEIGHT / 2;

  // sort sprites from far to close
  auto sprite_order = $level.collision->distance_sorted({(size_t)$pos_x, (size_t)$pos_y});

  // after sorting the sprites, do the projection
  for(auto& rec : sprite_order) {
    if(!$sprites.contains(rec.second)) continue;
    // BUG: eventually this needs to go away too
    auto& sf_sprite = $sprites.at(rec.second).sprite;
    auto sprite_pos = $level.world->get<components::Position>(rec.second);

    double sprite_x = double(sprite_pos.location.x) - $pos_x + 0.5;
    double sprite_y = double(sprite_pos.location.y) - $pos_y + 0.5;

    double inv_det = 1.0 / ($plane_x * $dir_y - $dir_x * $plane_y); // required for correct matrix multiplication

    double transform_x = inv_det * ($dir_y * sprite_x - $dir_x * sprite_y);

    //this is actually the depth inside the screen, that what Z is in 3D, the distance of sprite to player, matching sqrt(spriteDistance[i])
    double transform_y = inv_det * (-$plane_y * sprite_x + $plane_x * sprite_y);

    int sprite_screen_x = int(($width / 2) * (1 + transform_x / transform_y));

    // calculate the height of the sprite on screen
    //using "transform_y" instead of the real distance prevents fisheye
    int sprite_height = abs(int($height / transform_y));

    // calculate width the the sprite
    // same as height of sprite, given that it's square
    int sprite_width = abs(int($height / transform_y));

    int draw_start_x = -sprite_width / 2 + sprite_screen_x;
    if(draw_start_x < 0) draw_start_x = 0;
    int draw_end_x = sprite_width / 2 + sprite_screen_x;
    if(draw_end_x > $width) draw_end_x = $width;

    int stripe = draw_start_x;
    for(; stripe < draw_end_x; stripe++) {
      //the conditions in the if are:
      //1) it's in front of camera plane so you don't see things behind you
      //2) $zbuffer, with perpendicular distance
      if(!(transform_y > 0 && transform_y < $zbuffer[stripe])) break;
    }

    int tex_x_end = int(texture_width * (stripe - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width;

    if(draw_start_x < draw_end_x && transform_y > 0 && transform_y < $zbuffer[draw_start_x]) {
      //calculate lowest and highest pixel to fill in current stripe
      int draw_start_y = -sprite_height / 2 + $height / 2;
      if(draw_start_y < 0) draw_start_y = 0;
      int draw_end_y = sprite_height / 2 + $height / 2;
      if(draw_end_y >= $height) draw_end_y = $height - 1;

      int tex_x = int(texture_width * (draw_start_x - (-sprite_width / 2 + sprite_screen_x)) * texture_width / sprite_width) / texture_width;
      int tex_render_width = tex_x_end - tex_x;

      // avoid drawing sprites that are not visible (width < 0)
      if(tex_render_width <= 0) continue;

      float x = float(draw_start_x + $screen_pos_x);
      float y = float(draw_start_y + $screen_pos_y);
      float sprite_w = float(sprite_width) / float(texture_width);
      float sprite_h = float(sprite_height) / float(texture_height);

      int d = y * texture_height - $height * half_height + sprite_height * half_height;
      int tex_y = ((d * texture_height) / sprite_height) / texture_height;

      sf_sprite->setScale({sprite_w, sprite_h});
      $anim.step(*sf_sprite, tex_x, tex_y, tex_render_width, texture_height);
      sf_sprite->setPosition({x, y});

      $brightness.setUniform("offsetFactor", sf::Glsl::Vec2{0.0f, 0.0f});

      // the SpatialMap.distance_sorted only calculates the
      // (x1-x2)^2 + (y1-y2)^2 portion of distance, so to get
      // the actual distance we need to sqrt that.
      float level = sqrt(rec.first);
      $brightness.setUniform("darkness", level);
      target.draw(*sf_sprite, &$brightness);
    }
  }
}

void Raycaster::cast_rays() {
  constexpr static const int texture_width = TEXTURE_WIDTH;
  constexpr static const int texture_height = TEXTURE_HEIGHT;
  double perp_wall_dist;

  // WALL CASTING
  for(int x = 0; x < $width; x++) {
    // calculate ray position and direction
    double cameraX = 2 * x / double($width) - 1; // x-coord in camera space
    double ray_dir_x = $dir_x + $plane_x * cameraX;
    double ray_dir_y = $dir_y + $plane_y * cameraX;

    // which box of the map we're in
    int map_x = int($pos_x);
    int map_y = int($pos_y);

    // length of ray from one x or y-side to next x or y-side
    double delta_dist_x = std::abs(1.0 / ray_dir_x);
    double delta_dist_y = std::abs(1.0 / ray_dir_y);

    int step_x = 0;
    int step_y = 0;
    int hit = 0;
    int side = 0;

    // length of ray from current pos to next x or y-side
    double side_dist_x;
    double side_dist_y;

    if(ray_dir_x < 0) {
      step_x = -1;
      side_dist_x = ($pos_x - map_x) * delta_dist_x;
    } else {
      step_x = 1;
      side_dist_x = (map_x + 1.0 - $pos_x) * delta_dist_x;
    }

    if(ray_dir_y < 0) {
      step_y = -1;
      side_dist_y = ($pos_y - map_y) * delta_dist_y;
    } else {
      step_y = 1;
      side_dist_y = (map_y + 1.0 - $pos_y) * delta_dist_y;
    }

    // perform DDA
    while(hit == 0) {
      if(side_dist_x < side_dist_y) {
        side_dist_x += delta_dist_x;
        map_x += step_x;
        side = 0;
      } else {
        side_dist_y += delta_dist_y;
        map_y += step_y;
        side = 1;
      }

      if($map[map_y][map_x] > 0) hit = 1;
    }

    if(side == 0) {
      perp_wall_dist = (side_dist_x - delta_dist_x);
    } else {
      perp_wall_dist = (side_dist_y - delta_dist_y);
    }

    int line_height = int($height / perp_wall_dist);

    int draw_start = -line_height / 2 + $height / 2 + $pitch;
    if(draw_start < 0) draw_start = 0;

    int draw_end = line_height / 2 + $height / 2 + $pitch;
    if(draw_end >= $height) draw_end = $height - 1;

    auto texture = $textures.get_surface($map[map_y][map_x] - 1);

    // calculate value of wall_x
    double wall_x;  // where exactly the wall was hit
    if(side == 0) {
      wall_x = $pos_y + perp_wall_dist * ray_dir_y;
    } else {
      wall_x = $pos_x + perp_wall_dist * ray_dir_x;
    }
    wall_x -= floor(wall_x);

    // x coorindate on the texture
    int tex_x = int(wall_x * double(texture_width));
    if(side == 0 && ray_dir_x > 0) tex_x = texture_width - tex_x - 1;
    if(side == 1 && ray_dir_y < 0) tex_x = texture_width - tex_x - 1;

    // LODE: an integer-only bresenham or DDA like algorithm could make the texture coordinate stepping faster

    // How much to increase the texture coordinate per screen pixel
    double step = 1.0 * texture_height / line_height;
    // Starting texture coordinate
    double tex_pos = (draw_start - $pitch - $height / 2 + line_height / 2) * step;

    for(int y = draw_start; y < draw_end; y++) {
      int tex_y = (int)tex_pos & (texture_height - 1);
      tex_pos += step;
      RGBA pixel = texture[texture_height * tex_y + tex_x];
      $pixels[pixcoord(x, y)] = dumb_lighting(pixel, perp_wall_dist);
    }

    // SET THE ZBUFFER FOR THE SPRITE CASTING
    $zbuffer[x] = perp_wall_dist;
  }
}

void Raycaster::draw_ceiling_floor() {
  constexpr static const int texture_width = TEXTURE_WIDTH;
  constexpr static const int texture_height = TEXTURE_HEIGHT;

  for(int y = $height / 2 + 1; y < $height; ++y) {
    // rayDir for leftmost ray (x=0) and rightmost (x = w)
    float ray_dir_x0 = $dir_x - $plane_x;
    float ray_dir_y0 = $dir_y - $plane_y;
    float ray_dir_x1 = $dir_x + $plane_x;
    float ray_dir_y1 = $dir_y + $plane_y;

    // current y position compared to the horizon
    int p = y - $height / 2;

    // vertical position of the camera
    // 0.5 will the camera at the center horizon. For a
    // different value you need a separate loop for ceiling
    // and floor since they're no longer symmetrical.
    float pos_z = 0.5 * $height;

    // horizontal distance from the camera to the floor for the current row
    // 0.5 is the z position exactly in the middle between floor and ceiling
    // See NOTE in Lode's code for more.
    float row_distance = pos_z / p;

    // calculate the real world step vector we have to add for each x (parallel to camera plane)
    // adding step by step avoids multiplications with a wight in the inner loop
    float floor_step_x = row_distance * (ray_dir_x1 - ray_dir_x0) / $width;
    float floor_step_y = row_distance * (ray_dir_y1 - ray_dir_y0) / $width;


    // real world coordinates of the leftmost column.
    // This will be updated as we step to the right
    float floor_x = $pos_x + row_distance * ray_dir_x0;
    float floor_y = $pos_y + row_distance * ray_dir_y0;

    auto floor_texture = (const uint32_t *)$textures.floor.getPixelsPtr();
    auto ceiling_texture = (const uint32_t *)$textures.ceiling.getPixelsPtr();

    for(int x = 0; x < $width; ++x) {
      // the cell coord is simply taken from the int parts of
      // floor_x and floor_y.
      int cell_x = int(floor_x);
      int cell_y = int(floor_y);

      // get the texture coordinate from the fractional part
      int tx = int(texture_width * (floor_x - cell_x)) & (texture_width - 1);
      int ty = int(texture_width * (floor_y - cell_y)) & (texture_height - 1);

      floor_x += floor_step_x;
      floor_y += floor_step_y;

      double d = std::sqrt(($pos_x - floor_x) * ($pos_x - floor_x) + ($pos_y - floor_y) * ($pos_y - floor_y));

      // now get the pixel from the texture
      uint32_t color;
      // this uses the previous ty/tx fractional parts of
      // floor_x cell_x to find the texture x/y. How?

      // FLOOR
      color = floor_texture[texture_width * ty + tx];
      $pixels[pixcoord(x, y)] = dumb_lighting(color, d);

      // CEILING
      color = ceiling_texture[texture_width * ty + tx];
      $pixels[pixcoord(x, $height - y - 1)] = dumb_lighting(color, d);
    }
  }
}

void Raycaster::draw(sf::RenderTarget& target) {
  draw_ceiling_floor();
  cast_rays();
  draw_pixel_buffer();
  target.draw($view_sprite);
  sprite_casting(target);
}

void Raycaster::set_level(GameLevel level) {
  $level = level;
  auto& tiles = $level.map->tiles();
  auto world = $level.world;
  auto& player = world->get_the<components::Player>();
  $map = $textures.convert_char_to_texture(tiles.$tile_ids);

  world->query<components::Sprite>([&](const auto ent, auto& sprite) {
      // player doesn't need a sprite
      if(player.entity == ent) return;
      fmt::println("entity {} will have sprite named {}", ent, sprite.name);
      auto sprite_txt = $textures.sprite_textures.at(sprite.name);
      $sprites.try_emplace(ent, sprite_txt);
  });
}

void Raycaster::init_shaders() {
  dbc::check(sf::Shader::isAvailable(), "no shaders?!");
  bool good = $brightness.loadFromFile("shaders/modal.frag", sf::Shader::Type::Fragment);
  dbc::check(good, "shader could not be loaded");
  $brightness.setUniform("source", sf::Shader::CurrentTexture);
}
