#ifndef __WHITTED_RENDERER_H__
#define __WHITTED_RENDERER_H__

#include "AbstractRenderer.h"
#include "WhittedRendererWorker.h"
#include "geometry.h"

#define IOR_AIR 1.0f

namespace VRUT
{

	class WhittedRenderer : public AbstractRenderer
	{

	protected:

		int activatedWorkers; /// Count of workers activated last time
		wxAtomicInt activeWorkers; /// Count of workers currently working
		wxSemaphore notWorkingSemaphore;
		wxSemaphore firstLODSemaphore;

		std::vector<WhittedRendererWorker*> workers;

		Tile * tiles;
		unsigned int tileCount;
		std::atomic<unsigned int> tileCounter;
		int tileCounterStop;

		unsigned int lastSamples;
		wxULongLong lastTime;
		std::atomic<unsigned int> lod0ProcessedTiles;

		float prevAvgLuminace;

		int remoteTiles;
		int renderThreads;

		bool GetTile(unsigned int & tileIndex);

		void RenderTile(unsigned int tileIndex, const int workerIndex);

		void RenderRemoteTile(RenderTileData &tileData, const int workerIndex);

		void StartWorkers(int count);

		void WorkerFinished(int id);

	public:

		WhittedRenderer(RayTracer *rayTracer);
		virtual ~WhittedRenderer(void);

#ifndef USE_TEMPORAL
		virtual void Render(SceneShadow * sceneShadow, unsigned int camerasCount, RayTracerCamera ** cameras, unsigned int texWidth, unsigned int texHeight, float ** texData, RayTracerBHV * bvh, bool & sceneChanged, bool & lightChanged);
#else
		virtual void Render(SceneShadow * sceneShadow, unsigned int camerasCount, RayTracerCamera ** cameras, unsigned int texWidth, unsigned int texHeight, float *** texData, RayTracerBHV * bvh, bool & sceneChanged, bool & lightChanged);
#endif

		virtual void WaitForFirstLOD();

		virtual void RenderImage(SceneShadow * sceneShadow, RayTracerCamera *camera, unsigned int texWidth, unsigned int texHeight, const wxString &filename, RayTracerBHV *bvh, bool &sceneChanged, bool &lightChanged,
			bool save, bool addImage, int cubeSide, const wxString &imageRefName);

		virtual void RenderSlave(SceneShadow * sceneShadow, unsigned int camerasCount, RayTracerCamera ** cameras, RayTracerBHV * bvh, bool & sceneChanged, bool & lightChanged);

		virtual void WaitUntilAllWorkDone(void);

		/// count result color
		void Shade(const RayStream & rayStream, Vector4 bounceColors[], Vector4 diffuseColors[], Vector4 texColors[], bool usedTexs[], unsigned int cameraIndex, const int workerIndex);

		void ReflectedAndRefractedRays(const RayStream & inputRayStream, RayStream & outputRayStream, Vector4 colors[], Vector4 bounceColors[],  const int depth);

		/// cast packet of rays and count result color - used for multithreading
		virtual void CastAndShade(RayStream & rayStream, Vector4 colors[], unsigned int cameraIndex, const int workerIndex);

		void SetSlaveMode(const bool slaveMode);

		virtual void SetRemoteTiles(const int tiles) { remoteTiles = tiles; }

		virtual void SetRenderThreads(const int threads) { renderThreads = threads; }

		/// vector of received tiles - possible optimization  might be a list if multiple workers acquire the data
		std::queue< std::shared_ptr<RenderTileData> > receivedTiles;
		wxCriticalSection receivedTilesLock;
		std::atomic<int> remoteTilesCount;

		virtual unsigned int getTileCount() {
			return tileCount;
		}

		virtual void EnqueueReceivedTile(std::shared_ptr<RenderTileData> tileData)
		{
#if 1
			wxCriticalSectionLocker receivedTilesLocker(receivedTilesLock);
			receivedTiles.push(tileData);
#else
			const int id = tileData->workerId;
			if (id < workers.size()) {
				wxCriticalSectionLocker receivedTilesLocker(workers[id]->receivedTilesLock);
				workers[id]->receivedTiles.push(tileData);
			}
			//else
			//	LOG(wxString::Format(wxT("<RayTracer> EnqueueReceivedTile frameId=%i, tileIndex=%i, LOD=%i"), tileData->frameId, tileData->tileIndex, tileData->LOD));
#endif
		}

		virtual std::shared_ptr<RenderTileData> DequeueReceivedTile()
		{
			wxCriticalSectionLocker receivedTilesLocker(receivedTilesLock);

			if (receivedTiles.empty())
				return nullptr;
			else
			{
				auto r = receivedTiles.front();
				receivedTiles.pop();
				return r;
			}
		}

		virtual float GetAvgLuminance() 
		{
#if COMPUTE_LUMINANCE
			float avgLuminance = 0.0f;
			Vector2 sumLuminance = Vector2(0.0f, 0.0f);
			for (int i = 0; i < activatedWorkers; ++i)
				sumLuminance += workers[i]->sumLuminance;
			if (sumLuminance.y > 0.0f)
				avgLuminance = exp(sumLuminance.x / sumLuminance.y);
			const float alpha = 0.35f;
			avgLuminance = (1.0f - alpha) * avgLuminance + alpha * prevAvgLuminace;
			prevAvgLuminace = avgLuminance;
			//LOGERROR(wxString::Format("Avg luminance %f", avgLuminance));
			return avgLuminance;
#else
			return 0.5f;
#endif
		}

		void ToneMapReinhard(float &r, float &g, float &b, float keyValue, float whitePoint)
		{
			// RGB -> XYZ conversion
			/*const mat3 RGB2XYZ = mat3(0.5141364,  0.265068,   0.0241188,
									  0.3238786,  0.67023428, 0.1228178,
									  0.16036376, 0.06409157, 0.84442666);
			vec3 XYZ = RGB2XYZ * color;*/
			float X = 0.5141364f * r + 0.3238786f * g + 0.16036376f * b;
			float Y = 0.265068f * r + 0.67023428f * g + 0.06409157f * b;
			float Z = 0.0241188f * r + 0.1228178f * g + 0.84442666f * b;

			// XYZ -> Yxy conversion
			/*vec3 Yxy;
			Yxy.r = XYZ.g;                            // copy luminance Y
			Yxy.g = XYZ.r / (XYZ.r + XYZ.g + XYZ.b); // x = X / (X + Y + Z)
			Yxy.b = XYZ.g / (XYZ.r + XYZ.g + XYZ.b); // y = Y / (X + Y + Z)*/
			float tmp = X + Y + Z;
			float R = Y;
			float G = X / tmp;
			float B = Y / tmp;

			// (Lp) Map average luminance to the middlegrey zone by scaling pixel luminance
			//float Lp = Yxy.r * keyValue / avgLuminance;
			const float avgLuminance = 0.5f;
			keyValue *= 1.0e-2f;
			whitePoint *= 1.0e-2f;
			float Lp = R * keyValue / avgLuminance;
			// (Ld) Scale all luminance within a displayable range of 0 to 1
			//Yxy.r = (Lp * (1.0f + Lp/(whitePoint * whitePoint)))/(1.0f + Lp);
			R = (Lp * (1.0f + Lp/(whitePoint * whitePoint)))/(1.0f + Lp);

			// Yxy -> XYZ conversion
			/*XYZ.r = Yxy.r * Yxy.g / Yxy. b;               // X = Y * x / y
			XYZ.g = Yxy.r;                                // copy luminance Y
			XYZ.b = Yxy.r * (1 - Yxy.g - Yxy.b) / Yxy.b;  // Z = Y * (1-x-y) / y*/
			X = R * G / B;
			Y = R;
			Z = R * (1 - G - B) / B;

			// XYZ -> RGB conversion
			/*const mat3 XYZ2RGB  = mat3(2.5651, -1.0217,  0.0753,
									  -1.1665,  1.9777, -0.2543,
									  -0.3986,  0.0439,  1.1892);
			color = XYZ2RGB * XYZ;*/
			r = 2.5651f * X - 1.1665f * Y - 0.3986f * Z;
			g = -1.0217f * X + 1.9777f * Y + 0.0439f * Z;
			b = 0.0753f * X - 0.2543f * Y + 1.1892f * Z;
			//return color;
		}

		//Returns reflectivity
		float calculateFresnelReflectionRefractionRays(
			//In
			const Vector3& incidentRayDirection,
			const Vector3& intersectPoint,
			const Vector4& incidentRayWeight,
			const RTMaterial *material, 
			const float cosAngleInputRayNormal,
			const float bounceColorTransparency,
			const Vector3& normal,
			//Out
			Ray& reflectedRay,
			Vector4& reflectedRayWeight,
			Ray& refractedRay,
			Vector4& refractedRayWeight
			)
		{
			float tmpIOR1 = IOR_AIR;
			float tmpIOR2 = IOR_AIR;
			float reflectivityOrIOR = material->reflectivityOrIOR;
			const bool IORdefined = (material->flags & RTMaterial::HAS_IOR) ? true : false;
			refractedRayWeight = reflectedRayWeight = Vector4(0.0f, 0.0f, 0.0f, 0.0f);

			if (IORdefined)
			{
				if (cosAngleInputRayNormal > 0.0f)
					tmpIOR1 = material->reflectivityOrIOR;
				else
					tmpIOR2 = material->reflectivityOrIOR;
			}
			//the Fresnel computation: http://graphics.stanford.edu/courses/cs148-10-summer/docs/2006--degreve--reflection_refraction.pdf
			const float div_n1_n2 = tmpIOR1 / tmpIOR2;
			const float cos_phi1 = fabs(cosAngleInputRayNormal);
			const float sin2_phit = (div_n1_n2 * div_n1_n2) * (1.0 - (cos_phi1 * cos_phi1));
			
			//Refract
			if (sin2_phit < 1.0f)
			{
				if (IORdefined)
				{
					float cos_phit = sqrt(1.0f - sin2_phit);
					float Rort = (tmpIOR1 * cos_phi1 - tmpIOR2 * cos_phit) / (tmpIOR1 * cos_phi1 + tmpIOR2 * cos_phit);
					if (fullPolarization)
					{
						float Rpar = (tmpIOR2 * cos_phi1 - tmpIOR1 * cos_phit) / (tmpIOR2 * cos_phi1 + tmpIOR1 * cos_phit);
						reflectivityOrIOR = -((Rort * Rort + Rpar * Rpar) / 2.0f);
					}
					else
						reflectivityOrIOR = -Rort * Rort;
				}

				if (bounceColorTransparency < 1.0f)
				{
					float zavorka = div_n1_n2 * cos_phi1 - sqrt(1.0f - sin2_phit);
					Vector3 new_ray_refract = div_n1_n2 * incidentRayDirection + zavorka * normal;
					refractedRay = Ray(intersectPoint + new_ray_refract * rayOffset, new_ray_refract, FLT_MAX);
					refractedRayWeight = incidentRayWeight * (1.0f - bounceColorTransparency) * (1.0f - fabs(reflectivityOrIOR));
				}
			}
			else
			{
				// Total reflection.
				reflectivityOrIOR = 1.0f;
			}

			if (fabs(reflectivityOrIOR) > 1e-5f)
			{
				const Vector3 new_ray_direction = incidentRayDirection + (2 * cos_phi1) * normal;
				reflectedRay = Ray(intersectPoint + new_ray_direction * rayOffset, new_ray_direction, FLT_MAX);
				reflectedRayWeight = incidentRayWeight * fabs(reflectivityOrIOR);
			}

			return reflectivityOrIOR;
		}

		void processReflectRefractRayGetWeights(
			//In
			const Vector3& inputRayDirection,
			const Vector3& intersectPoint,
			const Vector3& normal,
			const RTMaterial* material,
			const int depth,
			const Vector4& weight,
			const unsigned int pixelIndex,
			const float cosAngleInputRayNormal,
			RayStream & outputRayStream,
			//In-out
			Vector4& bounceColor,
			//Out
			Vector4& reflectWeight,
			Vector4& refractWeight
			)
		{
			float reflectivityOrIOR = material->reflectivityOrIOR;
			reflectWeight = refractWeight = Vector4(0, 0, 0, 0);
			if ((depth < maxRecursionDepth) && ((bounceColor.w < 1.0f) || ((reflectivityOrIOR > 0.0f))))
			{
				Ray reflected, refracted;

				const float outReflectivity = calculateFresnelReflectionRefractionRays(inputRayDirection, intersectPoint, weight, material, cosAngleInputRayNormal, bounceColor.w, normal, reflected, reflectWeight, refracted, refractWeight);

				const bool isReflect = (reflectWeight.x + reflectWeight.y + reflectWeight.z + reflectWeight.w) != 0;
				const bool isRefract = (refractWeight.x + refractWeight.y + refractWeight.z + refractWeight.w) != 0;

				if (isRefract)
					outputRayStream.PushRay(refracted, pixelIndex, refractWeight);

				if (isReflect)
					outputRayStream.PushRay(reflected, pixelIndex, reflectWeight);

				bounceColor.w = !isRefract + (isRefract * bounceColor.w);
				bounceColor *= bounceColor.w * (1.0f - fabs(outReflectivity));
			}
			else
				bounceColor *= bounceColor.w;
		}

		friend class WhittedRendererWorker;
	};
}

#endif /* __WHITTED_RENDERER_H__ */
