/* raytracer.c */

#include "config.h"
#include "raytracer.h"

tuple camera_reflect(tuple normal, tuple incident) {
	return geo_add(
		geo_scale(normal, -2.0 * geo_dot(normal, incident)),
		incident
	);
}

tuple camera_refract(tuple normal, tuple incident, num n1, num n2, int* tir) {
	num n = n1 / n2;
	num cosI = geo_dot(normal, incident);
	num sinT2 = n * n * (1.0 - cosI * cosI);
	
	if (sinT2 > 1.0) {
		/*
#ifdef GEO_DEBUG
		fputs("WARNING: total internal reflection\n", stderr);
#endif
		*/
		*tir = 1;
		return geo_new(0,0,0);
	}
	*tir = 0;
	return geo_sub(		
		geo_scale(incident, n),
		geo_scale(normal, n*cosI + sqrt(1.0 - sinT2) )		
	);
}

tuple camera_blend(tuple c1, tuple c2, num ratio) {
	return geo_add(
		geo_scale(c1, 1.0 - ratio),
		geo_scale(c2, ratio)
	);
}

tuple camera_trace(camera *c, ray r, int level) {
		
	/* Perform hit test */
	
	object_intersection obj = {0};
	if (! object_search(c->scene, c->scene_count, r, &obj)) {
		return c->background;
	}
	
	/* Calculate diffuse colour */
	
	tuple object_colour = object_diffuse_colour(obj.obj, obj.p);
	
#ifdef ENABLE_LIGHTING

	/* Calculate lighting */	
	
	tuple sum = {0, 0, 0}; int i;		
	for (i = 0; i < c->light_count; i++) {
		
#ifdef ENABLE_SHADOWS

		/* Determine impact of shadow on this light */
		
		object_intersection eclipse = {0};
		ray eclipse_ray = NEW_RAY(obj.p, c->lights[i].dir);
		
		num shadowed = object_search(
			c->scene, c->scene_count, eclipse_ray, &eclipse
		)	? (1.0 - c->lights[i].intensity) : 1.0;
#else
		num shadowed = 1.0;	
#endif
		
		num m = geo_dot(obj.norm, c->lights[i].dir);
		if (m < 0) m = 0;
		
		/* Phong illumination with specular highlight */
		
#ifdef ENABLE_SPECULAR_HIGHLIGHTS		
		tuple h = geo_unit(geo_add(c->lights[i].dir, geo_neg(r.dir)));
		
		sum = geo_add(
			sum,
			geo_scale(
				geo_add(
					geo_scale(object_colour, m),
					geo_scale(
						obj.obj->mat.specular_colour,
						pow(geo_dot(h, obj.norm), obj.obj->mat.shininess)
					)
				),
				c->lights[i].intensity * shadowed
			)
		);
#else	
		sum = geo_add(
			sum,
			geo_scale(
				geo_scale(object_colour, m),
				c->lights[i].intensity * shadowed
			)
		);			
#endif

	}
	
	/* Blend ambient and lit colours */
	
	object_colour = geo_add(geo_scale(object_colour, c->light_ambient), sum);
	
#endif
	
	/* Recurse to find reflective and refractive components */
	
	if (level) {

#ifdef ENABLE_REFLECTION
	
		/* 1. Find reflective term  */
		
		if (obj.obj->mat.reflectivity) {
			
			tuple reflection_dir = camera_reflect(obj.norm, r.dir);
			ray reflected_ray = NEW_RAY(obj.p, reflection_dir);
			tuple reflective_term = camera_trace(
				c, reflected_ray, level - 1
			);
			
			/* Blend with existing colour */
			
			object_colour = camera_blend(
				object_colour, reflective_term, obj.obj->mat.reflectivity
			);
		
		}
		
#endif
#ifdef ENABLE_REFRACTION

		/* 2. Find refractive term */
		
		if (obj.obj->mat.refractivity) do {
			int tir;
		
			tuple refraction_dir = camera_refract(
				obj.norm, r.dir,
				AMBIENT_REFRACTIVE_INDEX,
				obj.obj->mat.refractive_index,
				&tir
			);
			
			if (tir) break;
			
			// Refract into the object
			
			ray refracted_ray = NEW_RAY(obj.p, refraction_dir);

			// Find the other interior edge
			
			tuple refractive_term; object_intersection far_edge = {0};
			
			if (! object_search(
				c->scene, c->scene_count, refracted_ray, &far_edge
			)) {
				refractive_term = c->background;
			} else {
			
				// Found the other edge. Refract out of the object
				
				tuple exit_dir = camera_refract(
					geo_neg(far_edge.norm),
					refraction_dir,
					AMBIENT_REFRACTIVE_INDEX,
					far_edge.obj->mat.refractive_index,
					&tir
				);
				if (tir) {
					refractive_term = camera_trace(
						c, refracted_ray, level - 1
					);
				} else {				
					ray exit_ray = NEW_RAY(far_edge.p, exit_dir);
					refractive_term = camera_trace(
						c, exit_ray, level - 1
					);
				}
				
			}
			
			/* Blend with existing colour */

			object_colour = camera_blend(
				object_colour, refractive_term, obj.obj->mat.refractivity
			);
			
		} while (0);
		
#endif
		
	}
	
	return object_colour;
}
