/*
 * $Id: RayTracerCamera.h 319 2011-11-09 15:15:15 sedald1 $
 *
 * Description  : Ray tracing camera
 * Author       : David Sedlacek
 *
 * Purpose      :
 *     Camera encapsulation for RayTracer
 */

#ifndef __RAYTRACERCAMERA_H__
#define __RAYTRACERCAMERA_H__

#include "3dmath.h"
#include "geometrypoints.h"


//	ray offset
#define CAMERA_RAY_OFFSET 0.005f

namespace VRUT
{
	class RayTracerCamera {
	public:
		Matrix cameraWorldMatrix;
		Matrix projectionMatrix;
		Matrix viewProjectionInverse;
		Matrix combinedMatrixNoTMTInv;

		Vector3 origin;
		Vector3 lookAt;

		RayTracerCamera() {}
		virtual ~RayTracerCamera() {}
		RayTracerCamera(const Matrix &vm, const Matrix &pm, const Vector3& viewDirection) : cameraWorldMatrix(vm), projectionMatrix(pm)
		{
			viewProjectionInverse = projectionMatrix.Inverse() * cameraWorldMatrix;
			lookAt = cameraWorldMatrix.TransformNormal(viewDirection);

			// FIXME: should be (0,0,0,1) instead? The transformed coordinates of both are equal.
			Vector4 tmpOrigin(viewProjectionInverse.TransformCoord(Vector4(0, 0, -1, 0)));
			origin.x = tmpOrigin.x / tmpOrigin.w;
			origin.y = tmpOrigin.y / tmpOrigin.w;
			origin.z = tmpOrigin.z / tmpOrigin.w;
		}

		virtual Ray PixelToRay(const float x, const float y, const float width, const float height) = 0;
	};

//***************************************************************************************************************//

	//referential implementation of PinholeCamera with barel distortion
	class PinholeCamera : public RayTracerCamera {

		//ohnisko - predpoklada se v pixelech
		//float f; - zatim nepouzito

		//radial distortion r(p) = 1.0 + k1 * ||p||^2 + k2 * ||p||^4.
		float k1;
		float k2;

		bool useRadialDistortion;

		// barel distortion
		float p1;
		float p2;

		// sample values of  a real camera
		// f = 1.8454269647e+003

		void InitializeSampleValues() {
			//N900
			//k1 = -0.28413736982;
			//k2 =  0.05581436219;

			//test
			//k1 = -3.7983656923e-001;
			//k2 = -1.3086079023e-002;

			//olympus + 8mm FishEye (FOV ~ 107)
			k1 = -0.21599538174f;
			k2 = 0.038143531935f;

			p1 = 0.0f;
			p2 = 0.0f;
		}

	public:
		PinholeCamera(const Matrix &vm, const Matrix &pm, const Vector3& lookAt, const float k1 = 0.0f, const float k2 = 0.0f, const bool useRadialDistortion = false)
			: RayTracerCamera(vm, pm, lookAt), k1(k1), k2(k2), useRadialDistortion(useRadialDistortion)
		{
			InitializeSampleValues();
		}

		// map pixel coordinates to a ray
		// the camera model should be independent of the resolution
		// so the res is passed as a parameter
		Ray PixelToRay(const float x, const float y, const float width, const float height) {
			//float px = ((( 2.0f * (x)) / width) - 1.0f) / projectionMatrix._m11;
			//float py = (((-2.0f * (y)) / height) + 1.0f) / projectionMatrix._m22;

			float px = 2*((x / width) - 0.5f);
			float py = 2*((-y / height) + 0.5f);

			// px, py are in the normalized device coordinates (-1,-1 to 1,1)
			float ppx=px, ppy=py;

			if (useRadialDistortion) {
				UndistortPixel(px, py, 0, 0, 1, width/height, k1,k2, p1,p2, &ppx, &ppy, 10);
			}
			//ppx, ppy are in the normalized device coordinates (-1,-1 to 1,1)
			Vector4 tmpDir(viewProjectionInverse.TransformCoord(Vector4(ppx, ppy, 1, 1)));
			return Ray(origin, Vector3(tmpDir.x/tmpDir.w-origin.x, tmpDir.y/tmpDir.w-origin.y, tmpDir.z/tmpDir.w-origin.z).Normalize());
		}

		void UndistortPixel(double  px, double  py, // souradnice pixelu
			double  cx, double  cy, // stred obrazku (v pixelech)
			double  fx, double  fy, // rozliseni (v pixelech)
			double  k1, double  k2, // parametry radial distortion (k1, k2)
			double  p1, double  p2, // parametry tangential distortion (p1, p2)
			float *tx, float *ty, // vysledne
			int max_iter)           // 10 iteraci OK
		{
			double x,y,xd,yd;

			x=xd=(px-cx)/fx;
			y=yd=(py-cy)/fy;

			for (int iter=0; iter<max_iter; iter++) {
				double x2=x*x;
				double y2=y*y;
				double xy=x*y;
				double r2=x2+y2;
				double r4=r2*r2;
				double ir=1./(1.+k1*r2+k2*r4);

				x=ir*(xd-2*p1*xy+p2*(r2+2*x2));
				y=ir*(yd-p1*(r2+2*y2)+2*p2*xy);
			}

			*tx=x*fx+cx;
			*ty=y*fy+cy;
		}

	};

//***************************************************************************************************************//

	//referential implementation of PinholeCamera with barel distortion
	class CylindricalCamera : public RayTracerCamera {

		//ohnisko - predpoklada se v pixelech
		//float f; - zatim nepouzito

		//radial distortion r(p) = 1.0 + k1 * ||p||^2 + k2 * ||p||^4.
		float k1;
		float k2;

		//horizontal FieldOfView (is different from camera FOV)
		float hFOV;

		//cylindrical camera had RadialDistortion only at vertical direction
		bool useRadialDistortion;

		// barel distortion
		float p1;
		float p2;

		// sample values of  a real camera
		// f = 1.8454269647e+003

		void InitializeSampleValues() {
			//N900
			//k1 = -0.28413736982;
			//k2 =  0.05581436219;

			//Horizon Panoramatic Camera
			//hFOV = 120;

			//test
			k1 = -3.7983656923e-001f;
			k2 = -1.3086079023e-002f;

			//olympus + 8mm FishEye (FOV ~ 107)
			//k1 = -0.21599538174;
			//k2 = 0.038143531935;

			p1 = 0.0f;
			p2 = 0.0f;
		}

	public:
		CylindricalCamera(const Matrix &vm, const Matrix &pm, const Vector3& lookAt, const float k1 = 0.0f, const float k2 = 0.0f, const float hFOV = 120,
			const bool useRadialDistortion = false) : RayTracerCamera(vm, pm, lookAt), k1(k1), k2(k2), hFOV((hFOV/180)*M_PI), useRadialDistortion(useRadialDistortion)
		{
			InitializeSampleValues();
		}

		// map pixel coordinates to a ray
		// the camera model should be independent of the resolution
		// so the res is passed as a parameter
		Ray PixelToRay(const float x, const float y, const float width, const float height) {
			//float px = ((( 2.0f * (x)) / width) - 1.0f) / projectionMatrix._m11;
			//float py = (((-2.0f * (y)) / height) + 1.0f) / projectionMatrix._m22;

			float px = 2*((x / width) - 0.5f);
			float py = 2*((-y / height) + 0.5f);
			float pz = -1.0f;

			// px, py are in the normalized device coordinates (-1,-1 to 1,1)
			float ppx=px, ppy=py, ppz = pz;

			if (useRadialDistortion == 1) {
				UndistortPixel(py, 0, width/height, k1, k2, p1, p2, &ppy, 10);
			}
			//ppx is in the normalized device coordinates (-1;1)
			//degree of pixel coresponding to the camera FOV
			float phi = px * hFOV/2;
			ppx = sin(phi);
			ppz = pz*cos(phi);

			//ppy is in the normalized device coordinates (-1,-1 to 1,1)
			//and multipliced by focal length (m11, m22)
			ppy = ppy / projectionMatrix._m22;
			Vector3 direction = cameraWorldMatrix.TransformNormal(Vector3(ppx, ppy, ppz)).Normalize();
			return Ray(origin, direction);
		}

		void UndistortPixel( double  py, // souradnice pixelu
			double  cy, // stred obrazku (v pixelech)
			double  fy, // rozliseni (v pixelech)
			double  k1, double  k2, // parametry radial distortion (k1, k2)
			double  p1, double  p2, // parametry tangential distortion (p1, p2)
			float *ty, // vysledne
			int max_iter)           // 10 iteraci OK
		{
			double y,yd,y2,r2,r4,ir;

			y=yd=(py-cy)/fy;

			for (int iter=0; iter<max_iter; iter++) {
				y2=y*y; r2=y2;
				r4=r2*r2; ir=1./(1.+k1*r2+k2*r4);

				y=ir*(yd-p1*(r2+2*y2));
			}

			*ty=y*fy+cy;
		}
	};

	//***************************************************************************************************************//

	//the ideal Spherical camera
	class SphericalCamera : public RayTracerCamera {

		//horizontal FieldOfView (is different from camera FOV)
		float hFOV;
		float vFOV;

	public:
		SphericalCamera(const Matrix &vm, const Matrix &pm, const Vector3& lookAt, const float hFOV = 360.0f, const float vFOV = 180.0f)
			: RayTracerCamera(vm, pm, lookAt), hFOV((hFOV/180)*M_PI), vFOV((vFOV/180)*M_PI) {}

		// map pixel coordinates to a ray
		// the camera model should be independent of the resolution
		// so the res is passed as a parameter
		Ray PixelToRay(const float x, const float y, const float width, const float height) {
			float px = 2*((x / width) - 0.5f);
			float py = 2*((-y / height) + 0.5f);
			float pz = -1.0f;
			// px, py are in the normalized device coordinates (-1,-1 to 1,1)
			//ppx is in the normalized device coordinates (-1;1)
			//ppy is in the normalized device coordinates (-1,-1 to 1,1)
			//degree of pixel corresponding to the camera FOV
			float phi = px * hFOV/2;
			float theta = py * vFOV/2;
			float ppx = sin(phi)*cos(theta);
			float ppy = sin(theta);
			float ppz = -cos(phi)*cos(theta);

			Vector3 direction = cameraWorldMatrix.TransformNormal(Vector3(ppx, ppy, ppz)).Normalize();
			return Ray(origin, direction);
		}

	};

	//***************************************************************************************************************//
	// ideal OmniCamera - orthogonal projection & sphere mirror
	class OmniOrthoSphereCamera : public RayTracerCamera {

	public:
		OmniOrthoSphereCamera(const Matrix &vm, const Matrix &pm, const Vector3& lookAt):RayTracerCamera(vm, pm, lookAt) {}

		// map pixel coordinates to a ray
		// the camera model should be independent of the resolution
		// so the res is passed as a parameter
		Ray PixelToRay(const float x, const float y, const float width, const float height) {
			//float px = ((( 2.0f * (x)) / width) - 1.0f) / projectionMatrix._m11;
			//float py = (((-2.0f * (y)) / height) + 1.0f) / projectionMatrix._m22;
			float px, py, pz;

			if (width > height) {
				px = 2*((x / height) - width/(2*height));
				py = 2*((-y / height) + 0.5f);
			} else {
				px = 2*((x / width) - 0.5f);
				py = 2*((-y / width) + height/(2*width));
			}
			pz = -1.0f;

			//if projection is out of sphere
			if ((px*px+py*py)>1) {
				px = 2*((x / width) - 0.5f);
				py = 2*((-y / height) + 0.5f);

				//Vector3 origin = cameraWorldMatrix.TransformNormal(Vector3(px, py, 1)).Normalize();
				//Vector3 origin = cameraWorldMatrix.TransformCoord(Vector3(0.0f, 0.0f, 0.0f));
				Vector3 origin = cameraWorldMatrix.TransformCoord(Vector3(px, py, 0.0f));
				Vector3 direction = cameraWorldMatrix.TransformNormal(Vector3(px, py, -1)).Normalize();
				return Ray(origin, direction);
			}

			// px, py are in the normalized device coordinates (-1,-1 to 1,1)
			float ppx=px, ppy=py, ppz = pz;

			//ppx is in the normalized device coordinates (-1;1)
			//degree of pixel coresponding to the camera FOV
			//float phi = px * hFOV/2;
			//float theta = py * vFOV/2;
			//ppx = sin(phi);
			//ppy = sin(theta);
			//ppz = pz*cos(phi);

			Vector3 direction = cameraWorldMatrix.TransformNormal(Vector3(ppx, ppy, ppz)).Normalize();
			Vector3 normal = Vector3(ppx,ppy, sqrtf(1-ppx*ppx-ppy*ppy));
			float dotProductRay = -normal.Dot(direction);

			Vector3 rdir = (2.0f * dotProductRay * normal + direction).Normalize();
			// cast reflected ray
			return Ray(normal+CAMERA_RAY_OFFSET*rdir, rdir);

			//return Ray(origin, direction);
		}
	};

	//Creates 360 deg. stereo panorama
	class Spherical3DPanoramaCamera : public RayTracerCamera
	{
	public:
		Spherical3DPanoramaCamera(const Matrix &vm, const Matrix &pm, const Vector3& lookAt)
			: RayTracerCamera(vm, pm, lookAt)
		{}

		~Spherical3DPanoramaCamera() {}

		Ray PixelToRay(const float x, const float y, const float width, const float height)
		{
			//Parameters
			//origin = Vector3(0, 0, -20);
			const float ipd = 65;
			const float hFOV = static_cast<float>(2*M_PI);
			const float vFOV =   static_cast<float>(M_PI);
			const float sphereRadius = 1950.0f;

			const bool isRight = y < (height / 2);

			float px = 2*((x / width) - 0.5f);
			float py = 2*((-y / (height/2)) + 0.5f);

			float phi = px * hFOV/2;
			float theta = py * vFOV/2;

			float ppx = sin(phi)*cos(theta);
			float ppy = sin(theta);
			float ppz = -cos(phi)*cos(theta);

			const Vector3 view = Normalize(Vector3(ppx, ppy, ppz));
			const Vector4 focusPoint = Vector4(sphereRadius * ppx, sphereRadius * ppy, sphereRadius * ppz, 1) + Vector4(origin, 0);

			//Calc position of left and right eye
			const Vector3 left = Normalize(CrossProd(Vector3(0, 1, 0), view));

			const Vector4 posL = Vector4(origin, 1) + Vector4(-left*(ipd/2), 0);
			const Vector4 posR = Vector4(origin, 1) + Vector4( left*(ipd/2), 0);

			Vector3 direction = Normalize(Vector3(focusPoint) - Vector3(isRight ? posR : posL));

			return Ray(Vector3(isRight ? posR : posL), direction);
		}
	};
}

#endif
