#ifndef _H_RAYCASTER_H
#define _H_RAYCASTER_H

#include <glm/glm.hpp>
#include <vector>
#include <memory>

#include "utility/debugdraw.h"

class Raycaster {
public:
	Raycaster(
		float FOV, 
		float viewDist, 
		const std::vector<std::vector<int>>& collisionMap, 
		float tileSize) 
		: 
		FOV(FOV), 
		viewDist(viewDist),
		collisionMap(collisionMap),
		tileSize(tileSize)
	{}
	bool bresenhamRaycast(const glm::vec3& origin, const float rotation, const glm::vec3& target)
	{
		float rayAngle = rotation - (FOV / 2);
		float angleIncrement = 2.0f;
		bool hitTarget = false;
		glm::ivec2 lastEnd = glm::ivec2(0);
		for (int rayCount = 0; rayCount <= FOV / 2; rayCount++)
		{
			float endX = (origin.x + (glm::cos(glm::radians(rayAngle)) * viewDist));
			float endY = (origin.y + (glm::sin(glm::radians(rayAngle)) * viewDist));

			glm::ivec2 targetTile= glm::ivec2(static_cast<int>(target.x / tileSize), static_cast<int>(target.y / tileSize));
			glm::ivec2 startTile = glm::ivec2(static_cast<int>((origin.x + tileSize * 0.1f) / tileSize), static_cast<int>((origin.y + tileSize * 0.1f) / tileSize));
			glm::ivec2 endTile	 = glm::ivec2(static_cast<int>(endX / tileSize), static_cast<int>(endY / tileSize));

			int dx = glm::abs(endTile.x - startTile.x);
			int dy = glm::abs(endTile.y - startTile.y);
			int sx = startTile.x < endTile.x ? 1 : -1;
			int sy = startTile.y < endTile.y ? 1 : -1;

			int err = (dx > dy ? dx : -dy) / 2;

			glm::ivec2 step = startTile;

			while (step != endTile)
			{
				if (step.x < 0 || step.x >= collisionMap[0].size() ||
					step.y < 0 || step.y >= collisionMap.size())
				{
					if (glm::abs(rayAngle - rotation) <= 2)
						distFromWall = distFromWall < glm::distance(glm::vec2(startTile), glm::vec2(step)) ?
						distFromWall : glm::distance(glm::vec2(startTile), glm::vec2(step));
					break;
				}
				if (collisionMap[step.y][step.x] != 0)
				{
					if (glm::abs(rayAngle - rotation) <= 2)
						distFromWall = distFromWall < glm::distance(glm::vec2(startTile), glm::vec2(step)) ?
						distFromWall : glm::distance(glm::vec2(startTile), glm::vec2(step));
					break;
				}
				if (step == targetTile)
					hitTarget = true;
				int e2 = err;
				if (e2 > -dx)
				{
					err -= dy;
					step.x += sx;
				}
				if (e2 < dy)
				{
					err += dx;
					step.y += sy;
				}
			}
			//LineDrawer::getInstance().addLine(origin, glm::vec3(step.x * tileSize, step.y * tileSize, 0.f), glm::vec4(1.0 - (1.0f / rayCount), 1.0f / rayCount, 0.0f, 0.7f));
			if (step == endTile)
				distFromWall = std::numeric_limits<float>::infinity();
			rayAngle += angleIncrement;
		}
		return hitTarget;
	}
	// We need to collision map and tile size so we can hide behind tiles
	// returns true if the raycast lands on the targets tile
	bool performRaycast(const glm::vec3& origin, const float rotation, const glm::vec3& target) {
		float rayStepSize = 0.05f;
		float rayAngle = rotation - (FOV / 2);
		float angleInc = 2.0f;
		glm::ivec2 targetTile = glm::ivec2(static_cast<int>(target.x / tileSize), static_cast<int>(target.y / tileSize));
		bool hitTarget = false;
		for (int rayCount = 0; rayCount <= FOV / 2; rayCount++)
		{
			glm::vec2 ray = glm::vec2(origin);
			while (glm::distance(ray, glm::vec2(origin)) < viewDist)
			{
				ray.x += cos(glm::radians(rayAngle)) * rayStepSize;
				ray.y += sin(glm::radians(rayAngle)) * rayStepSize;
				glm::ivec2 rayTile = glm::ivec2(static_cast<int>(ray.x / tileSize), static_cast<int>(ray.y / tileSize));
				if (rayTile.x < 0 || rayTile.x >= collisionMap[0].size() ||
					rayTile.y < 0 || rayTile.y >= collisionMap.size())
				{
					if (rayCount == FOV / 2)
						distFromWall = glm::distance(glm::vec2(origin), ray);
					break;
				}
				if (collisionMap[rayTile.y][rayTile.x] != 0)
				{
					if (rayCount == FOV / 2)
						distFromWall = glm::distance(glm::vec2(origin), ray);
					break;
				}
				if (rayTile == targetTile)
					hitTarget = true;
			}
			DebugDrawer::getInstance().addLine(origin, glm::vec3(ray.x, ray.y, 0.f), glm::vec4(1.0f, 0.0f, 0.0f, 0.2f));
			if (glm::distance(ray, glm::vec2(origin)) >= viewDist)
				distFromWall = std::numeric_limits<float>::infinity();
			rayAngle += angleInc;
		}
		return hitTarget;
	}
	float getDistanceFromWall() const { return distFromWall; }
	float getTileSize() const { return tileSize; }
private:
	float FOV;
	float viewDist;
	float distFromWall = std::numeric_limits<float>::infinity();
	std::vector<std::vector<int>> collisionMap;
	float tileSize;

};

#endif // _H_RAYCASTER_H