#include <math.h>
#include <stdio.h>
#include "intersection.h"
#include "v3math.h"
#include "constants.h"
#include "scene.h"

// A few counters for gathering statistics on the number and types
// of ray shot

int num_rays_shot = 0;
int num_shadow_rays_shot = 0;
int num_triangles_tested = 0;

// Checks if the given triangle is intersected by ray with given
// origin and direction.
//
// Returns 1 if there is an intersection, or 0 otherwise.
//
// When an intersection is found the variables t_hit, p_isec and n_isec
// are set to the relevant ray parameter, intersection point and
// normal at the intersection point, respectively. The latter is
// simply the triangle normal.
//
// Note: this routine does NOT return an intersection for triangles
// whose back side faces the ray (by definition a triangle normal
// points to the triangle's front side).
// I.e. we do back-face culling here ...

static int
ray_intersects_triangle(intersection_point* ip, triangle tri,
    vec3 ray_origin, vec3 ray_direction)
{
    float       denom;
    float       t_plane;
    int         i, j, k;
    vec3        p;

    // Increase the counter here, as we might not reach the end of
    // this function, due to early bailouts
    num_triangles_tested++;

    // Check ray direction against triangle normal

    denom = v3_dotprod(ray_direction, tri.n);

    if (denom > -C_EPSILON && denom < C_EPSILON)
    {
        // No hit possible, because the triangle and ray are
        // (almost) perpendicular
        return 0;
    }

    if (denom > 0)
    {
        // The back-side of the triangle faces the ray, no
        // intersection.
        // And yes, we could have combined this test with the one
        // above...
        return 0;
    }

    // Get the vertex indices for the corners of this triangle
    i = tri.v[0];
    j = tri.v[1];
    k = tri.v[2];

    // Compute the ray value where the ray intersects the plane defined by the triangle
    t_plane = - v3_dotprod(v3_subtract(ray_origin, scene_vertices[i]), tri.n) / denom;

    // Plane intersection point is "behind" the ray origin, no hit possible
    if (t_plane < 0.0)
        return 0;

    // Compute actual intersection point of ray and triangle plane
    p = v3_add(ray_origin, v3_multiply(ray_direction, t_plane));

    // Check if the plane intersection point is within the boundaries of the triangle

    if  (v3_dotprod(
            v3_crossprod(v3_subtract(scene_vertices[j], scene_vertices[i]), v3_subtract(p, scene_vertices[i])),
            tri.n) < 0.0)
        return 0;

    if  (v3_dotprod(
            v3_crossprod(v3_subtract(scene_vertices[k], scene_vertices[j]), v3_subtract(p, scene_vertices[j])),
            tri.n) < 0.0)
        return 0;

    if  (v3_dotprod(
            v3_crossprod(v3_subtract(scene_vertices[i], scene_vertices[k]), v3_subtract(p, scene_vertices[k])),
            tri.n) < 0.0)
        return 0;

    // We have a triangle intersection!
    // Return the relevant intersection values.

    ip->t = t_plane;
    ip->p = p;
    ip->n = tri.n;
    ip->i = v3_normalize(v3_negate(ray_direction));
    ip->material = tri.material;

    return 1;
}

// Check if the given sphere is intersected by the given ray.
// See Shirley et.al., section 10.3.1
// Returns 1 if there is an intersection (and set the appropriate
// values of ip), or 0 otherwise.
static int
ray_intersects_sphere(intersection_point* ip, sphere sph,
    vec3 ray_origin, vec3 ray_direction)
{
    float   A, B, C, D;
    vec3    diff;
    float   t_hit;

    A = v3_dotprod(ray_direction, ray_direction);

    diff = v3_subtract(ray_origin, sph.center);
    B = 2.0 * v3_dotprod(diff, ray_direction);
    C = v3_dotprod(diff, diff) - sph.radius * sph.radius;

    D = B*B - 4*A*C;

    if (D < 0.0)
        return 0;

    D = sqrt(D);

    t_hit = (-B - D)/(2*A);

    if (t_hit < 0.0)
    {
        t_hit = (-B + D)/(2*A);
        if (t_hit < 0.0)
            return 0;
    }

    ip->t = t_hit;
    ip->p = v3_add(ray_origin, v3_multiply(ray_direction, t_hit));
    ip->n = v3_normalize(v3_subtract(ip->p, sph.center));
    ip->i = v3_normalize(v3_negate(ray_direction));
    ip->material = sph.material;

    return 1;
}

// Optimized routine for tracing a shadow ray.
//
// This routine doesn't return the nearest intersection, but simply
// checks if there is any intersection.
int
shadow_check(vec3 ray_origin, vec3 ray_direction)
{
    intersection_point  ip;

    num_rays_shot++;
    num_shadow_rays_shot++;

    for (int s = 0; s < scene_num_spheres; s++)
    {
        if (ray_intersects_sphere(&ip, scene_spheres[s], ray_origin, ray_direction))
            return 1;
    }

    for (int t = 0; t < scene_num_triangles; t++)
    {
        if (ray_intersects_triangle(&ip, scene_triangles[t], ray_origin, ray_direction))
            return 1;
    }

    return 0;
}

// Returns the nearest hit of the given ray with objects in the scene
// (either a sphere or a triangle).
//
// Returns 1 and set the intersection point values if there
// is an intersection, returns 0 otherwise.
int
find_first_intersection(intersection_point *ip, vec3 ray_origin, vec3 ray_direction)
{
    int     s, t;
    int     have_hit;
    float   t_nearest;
    intersection_point  ip2;

    num_rays_shot++;

    // We have found no hit yet
    t_nearest = C_INFINITY;
    have_hit = 0;

    // First check against spheres in the scene
    for (s = 0; s < scene_num_spheres; s++)
    {
        // We need a second set of p and n variables, as there's the
        // possibility that we'll overwrite a closer intersection already
        // found
        if (ray_intersects_sphere(&ip2, scene_spheres[s], ray_origin, ray_direction))
        {
            if (ip2.t < t_nearest)
            {
                *ip = ip2;
                t_nearest = ip2.t;
                have_hit = 1;
            }
        }
    }

    // Then check against triangles in the scene
    for (t = 0; t < scene_num_triangles; t++)
    {
        if (ray_intersects_triangle(&ip2, scene_triangles[t], ray_origin, ray_direction))
        {
            if (ip2.t < t_nearest)
            {
                *ip = ip2;
                t_nearest = ip2.t;
                have_hit = 1;
            }
        }
    }

    return have_hit;
}

