#ifndef ReflectiveTexturedPhongShader_HXX
#define ReflectiveTexturedPhongShader_HXX

#include "PhongShader.hxx"
// maximum number of reflections, this should be enough for most realistic images
#define MAXRefCounter 3


/* Shader
 *
 * Implements reflection and refraction properties
 *
 * Miguel Granados, Richard Socher
 * Core Lecture in Computer Graphics by Prof. Dr. Philipp Slusallek
 * University of Saarland
 * WS2006/2007
 */

class ReflectiveTexturedPhongShader: public Shader
{
public:

	Vec3f Ia;    // amblient light intensity;
	Material defaultMaterial;

	ReflectiveTexturedPhongShader(Scene *scene) : Shader(scene), Ia(0){}

	virtual Vec3f Shade(Ray &ray) const
	{
		const Material *material;
		if(!ray.hit->material)
			material = &defaultMaterial;
		else
			material = ray.hit->material;

		/* constant illumination model */
		if(material->illum == 0) {
			if(!material->mapKd)
				return material->Kd;
			float u, v;
			ray.hit->GetUV(ray, u, v);
			return material->mapKd->GetTexel(u, v);
		}

	    /* get shading normal */
	    Vec3f normal = ray.hit->GetNormal(ray);
	    if(!(fabs(Length(normal) - 1.0f) < Epsilon))
	    	cerr << "error";

	    /* turn normal to front */
	    if (Dot(normal,ray.dir) > 0)
    	  normal = -normal;
		if(!(fabs(Length(ray.dir) - 1.0f) < Epsilon))
			cerr << "error";
    	if(!(Dot(ray.dir, normal) <= 0))
    		cerr << "error";

	    /* calculate reflection vector */
	    Vec3f reflectionDir = ray.dir - 2*Dot(normal,ray.dir)*normal;
	    if(!(fabs(Length(reflectionDir) - 1.0f) < Epsilon))
	    	cerr << "error";

	    /* ambient term */
	    Vec3f ambientIntensity = Product(material->Ka, Ia);

		/* diffuse and specular illumination model */
		Vec3f diffuseIntensity, specularIntensity;
		Diffusion(ray, normal, reflectionDir, material, diffuseIntensity, specularIntensity);
		if(material->illum == 1)
			return ambientIntensity + diffuseIntensity;
		if(material->illum == 2)
			return ambientIntensity + diffuseIntensity + specularIntensity;

		/* diffuse and specular illumination model + reflection term */
		Vec3f reflectionIntensity = Reflection(ray, reflectionDir);
		if(material->illum == 3 || material->illum == 4)
			return ambientIntensity + diffuseIntensity + specularIntensity + Product(material->Ks, reflectionIntensity);

		/*
			// get incidence (and reflection) angle
			float reflectionAngle = acos(Dot(normal, -ray.dir));


			// fresnel equation
			float reflectionCoeff = 0.5 * ( powf(sin(reflectionAngle - refractionAngle), 2) / powf(sin(reflectionAngle + refractionAngle), 2) +
											powf(tan(reflectionAngle - refractionAngle), 2) / powf(tan(reflectionAngle + refractionAngle), 2));
			float refractionCoeff = 1 - reflectionCoeff;
		*/

		/* diffuse and specular illumination model + reflection +  refraction*/
		if(material->illum == 6) {
			float refractionAngle;
			Vec3f refractionIntensity = Refraction(ray, reflectionDir, refractionAngle);
			return ambientIntensity + diffuseIntensity + specularIntensity + Product(material->Ks, reflectionIntensity) + Product(1-material->Ks, refractionIntensity);
		}

		assert(false); // unknown illumination model
		return Vec3f(0); // never reached
    }


  private:
  	void Diffusion(Ray ray, const Vec3f& normal, const Vec3f& reflectionDir, const Material* material, Vec3f& diffuseColor, Vec3f& specularColor) const { // TODO: make ray const
  		diffuseColor = Vec3f(0);
  		specularColor = Vec3f(0);

	    /* shadow ray (up to now only for the light direction) */
	    Ray shadow;
	    shadow.org = ray.org + ray.t * ray.dir; // TODO: add Epsilon?

	    /* iterate over all light sources */
	    for (unsigned int l=0; l<scene->lights.size(); l++) {

	      /* get direction to light, and intensity */
	      Vec3f lightIntensity;

	      if (scene->lights[l]->Illuminate(shadow, lightIntensity)) {

		    if(!(fabs(Length(shadow.dir) - 1.0f) < Epsilon))
		    	cerr << "error";

			/* diffuse term */
			float cosLightNormal = Dot(shadow.dir, normal);
			if (cosLightNormal > 0) {
			  if (scene->Occluded(shadow))
			    continue;

				Vec3f kd;
				if(!material->mapKd)
					kd = material->Kd;
				else {
					float u, v;
					ray.hit->GetUV(ray, u, v);
					kd = material->mapKd->GetTexel(u, v);
				}

			  diffuseColor += cosLightNormal * Product(kd, lightIntensity);
			}

			/* specular term */
			float cosLightReflect = Dot(shadow.dir, reflectionDir);
			if (cosLightReflect > 0) {
			  specularColor += powf(cosLightReflect, material->Ns) * Product(material->Ks, lightIntensity);
			}
		  }
  		}
  	}

  	Vec3f Refraction(Ray ray, const Vec3f& reflectionDir, float& refractionAngle) const { // ray should be const

		// get index of refraction of the material when comming from air
		const Material* material = ray.hit->material;
		assert(material != NULL);
		float Ni = material->Ni;

		assert(Ni > 0 && Ni <= 10); // by definition of MTL file

		Vec3f normal = ray.hit->GetNormal(ray);
		if(Dot(normal, ray.dir) < 0) {
			// comming from air to inside the object
			Ni = 1/Ni;
			normal = -normal;
		} else {
			 // comming from inside the object to the air
		}
		float Ni2 = Ni*Ni;

		Ray refractedRay;
		refractedRay.org = ray.org + ray.t * ray.dir; // TODO: add Epsilon
		refractedRay.t = Infinity;
		refractedRay.hit = NULL;

		//Snell's law
		float cosOut, cosIn;
		cosIn = Dot(normal, ray.dir);
		float sinIn2 = 1 - (cosIn*cosIn); //1=sin²x+cos²(x)
		if (Ni2 * sinIn2 > 1) {
			// no refraction, only reflection
			refractionAngle = 0;
			return Vec3f(0); // delayed reflection
		}
		else {
			cosOut = sqrt(1 - (Ni2 * sinIn2));
			refractedRay.dir = Ni * ray.dir + (cosOut - Ni*cosIn) * normal;
			Normalize(refractedRay.dir);
		    if(!(fabs(Length(refractedRay.dir) - 1.0f) < Epsilon))
		    	cerr << "error";
		    if(!(Dot(ray.dir, refractedRay.dir) >= 0)) {
		    	cerr << "error";
		    	return Vec3f(1,0,1);
		    }
			refractionAngle = acos(fabs(cosOut));
			return scene->RayTrace(refractedRay);
		}
  	}

  	virtual Vec3f Reflection(const Ray& ray, const Vec3f& reflectionDir) const {
		Ray reflectedRay;
		reflectedRay.org = ray.org + ray.t * ray.dir;  // TODO: should we include Epsilon?
		reflectedRay.dir = reflectionDir;
		reflectedRay.t = Infinity;
		reflectedRay.hit = NULL;
		return scene->RayTrace(reflectedRay);
  	}
};
#endif

