#pragma once
#include <fstream>
#include "Maths.h"
#include "DataTypes.h"

namespace dae
{
	namespace GeometryUtils
	{
#pragma region Sphere HitTest
		//SPHERE HIT-TESTS
		inline bool HitTest_Sphere(const Sphere& sphere, const Ray& ray, HitRecord& hitRecord, bool ignoreHitRecord = false)
		{
            const float radius2 = sphere.radius * sphere.radius;
            const Vector3 CameraToOrigin = sphere.origin - ray.origin;
            const float tInSphere = Vector3::Dot(CameraToOrigin, ray.direction);

            if(tInSphere < 0) return false; //Looking away from sphere

            float originToInsideDist2 = CameraToOrigin.SqrMagnitude() - tInSphere * tInSphere;
            if(originToInsideDist2 > radius2)
                return false; // 'inside' point is outside of sphere

            float tDiff = std::sqrt(radius2 - originToInsideDist2);
            float t = tInSphere - tDiff;

            const bool hit = t > ray.min && t < ray.max;


            if (!ignoreHitRecord && hit){
                hitRecord.t = t;
                hitRecord.origin = ray.origin + ray.direction * t;
                hitRecord.normal = (hitRecord.origin - sphere.origin).Normalized();
                hitRecord.materialIndex = sphere.materialIndex;
                hitRecord.didHit = true;

            }

            return hit;

		}

		inline bool HitTest_Sphere(const Sphere& sphere, const Ray& ray)
		{
			HitRecord temp{};
			return HitTest_Sphere(sphere, ray, temp, true);
		}
#pragma endregion
#pragma region Plane HitTest
		//PLANE HIT-TESTS
		inline bool HitTest_Plane(const Plane& plane, const Ray& ray, HitRecord& hitRecord, bool ignoreHitRecord = false)
		{
            float dot = Vector3::Dot(plane.normal, ray.direction);
            if(dot < 0) {
//                float t = Vector3::Dot(plane.origin - ray.origin, plane.normal) / dot;
                float t = Vector3::Dot(plane.origin - ray.origin, plane.normal);
                t /= dot;

                const bool hit = t > ray.min && t < ray.max;

                if(hit && !ignoreHitRecord){
                    hitRecord.t = t;
                    hitRecord.origin = ray.origin + ray.direction * t;
                    hitRecord.normal = plane.normal;
                    hitRecord.materialIndex = plane.materialIndex;
                    hitRecord.didHit = true;
                }

                return hit;
            }

            return false;
		}

		inline bool HitTest_Plane(const Plane& plane, const Ray& ray)
		{
			HitRecord temp{};
			return HitTest_Plane(plane, ray, temp, true);
		}
#pragma endregion
#pragma region Triangle HitTest
		//TRIANGLE HIT-TESTS
		inline bool HitTest_Triangle(const Triangle& triangle, const Ray& ray, HitRecord& hitRecord, bool ignoreHitRecord = false)
		{
			//todo W5
			throw std::runtime_error("Not Implemented Yet");
			return false;
		}

		inline bool HitTest_Triangle(const Triangle& triangle, const Ray& ray)
		{
			HitRecord temp{};
			return HitTest_Triangle(triangle, ray, temp, true);
		}
#pragma endregion
#pragma region TriangeMesh HitTest
		inline bool HitTest_TriangleMesh(const TriangleMesh& mesh, const Ray& ray, HitRecord& hitRecord, bool ignoreHitRecord = false)
		{
			//todo W5
			throw std::runtime_error("Not Implemented Yet");
			return false;
		}

		inline bool HitTest_TriangleMesh(const TriangleMesh& mesh, const Ray& ray)
		{
			HitRecord temp{};
			return HitTest_TriangleMesh(mesh, ray, temp, true);
		}
#pragma endregion
	}

	namespace LightUtils
	{
		//Direction from target to light
		inline Vector3 GetDirectionToLight(const Light& light, const Vector3 origin)
		{
            return { light.origin - origin };
		}

		inline ColorRGB GetRadiance(const Light& light, const Vector3& target)
		{
            switch(light.type)
            {
                case LightType::Point:
                    return light.color * light.intensity / (light.origin - target).SqrMagnitude();
                case LightType::Directional:
                    return light.color * light.intensity;
            }
		}

        inline float LambertCosineLaw(const Vector3& normal, const Vector3& lightDirection)
        {
            return Vector3::Dot(normal, lightDirection);
        }
	}

	namespace Utils
	{
		//Just parses vertices and indices
#pragma warning(push)
#pragma warning(disable : 4505) //Warning unreferenced local function
		static bool ParseOBJ(const std::string& filename, std::vector<Vector3>& positions, std::vector<Vector3>& normals, std::vector<int>& indices)
		{
			std::ifstream file(filename);
			if (!file)
				return false;

			std::string sCommand;
			// start a while iteration ending when the end of file is reached (ios::eof)
			while (!file.eof())
			{
				//read the first word of the string, use the >> operator (istream::operator>>) 
				file >> sCommand;
				//use conditional statements to process the different commands	
				if (sCommand == "#")
				{
					// Ignore Comment
				}
				else if (sCommand == "v")
				{
					//Vertex
					float x, y, z;
					file >> x >> y >> z;
					positions.push_back({ x, y, z });
				}
				else if (sCommand == "f")
				{
					float i0, i1, i2;
					file >> i0 >> i1 >> i2;

					indices.push_back((int)i0 - 1);
					indices.push_back((int)i1 - 1);
					indices.push_back((int)i2 - 1);
				}
				//read till end of line and ignore all remaining chars
				file.ignore(1000, '\n');

				if (file.eof())
					break;
			}

			//Precompute normals
			for (uint64_t index = 0; index < indices.size(); index += 3)
			{
				uint32_t i0 = indices[index];
				uint32_t i1 = indices[index + 1];
				uint32_t i2 = indices[index + 2];

				Vector3 edgeV0V1 = positions[i1] - positions[i0];
				Vector3 edgeV0V2 = positions[i2] - positions[i0];
				Vector3 normal = Vector3::Cross(edgeV0V1, edgeV0V2);

				if (std::isnan(normal.x))
				{
					int k = 0;
				}

				normal.Normalize();
				if (std::isnan(normal.x))
				{
					int k = 0;
				}

				normals.push_back(normal);
			}

			return true;
		}
#pragma warning(pop)
	}
}